tell a member why their agent is not working, instead of 403
f0af723granted chat to every role by default, which is right, but the route still refuses non-owners — so a new member gets a tile that resolves and an API that 403s, the exact broken stateb4f88ecandeda004awere built to remove. The fix is not to withdraw the grant. It is to answer the question the member actually has, which is "what do I do about it": their own claude, in their own home, needs them to sign in once with their own Anthropic account. The platform cannot do that for them — logging in is an interactive act against an account that is theirs, and the alternative, pointing them at the owner's credential proxy, spends the owner's subscription on their turns. GET /api-status returns two booleans about the caller's own home plus the one instruction that fits their case, so the UI can render a terminal saying "run claude once" rather than an error. Its own router, deliberately not on chatRouter: that router refuses every non-owner wholesale and is right to — reads there leak the owner's project directory names — which means an endpoint on it could not be read by the accounts that need it most. Same `chat` capability, no owner gate, and nothing in the response describes anyone but the caller. Frontend not done: nothing calls this yet, so behaviour is still unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { getUserById } from 'officerdb';
|
||||
import { claudeLoginState } from '@@/os-user-claude';
|
||||
import { resolveHomeDir } from '@@/user-home';
|
||||
import { createRouter } from '../../create-router';
|
||||
|
||||
// Can this account actually run an agent, and if not, what does it need to do about it?
|
||||
//
|
||||
// ── Why this is not in `api/chat` ──
|
||||
//
|
||||
// It would be the obvious home, and it cannot go there: `chat.ts:49` refuses every non-owner wholesale, so an
|
||||
// endpoint on that router could not be read by the accounts that most need it. The refusal is right — reads
|
||||
// there leak the owner's project directory names — but it means the answer to "why is my agent not working"
|
||||
// has to live somewhere a member can reach.
|
||||
//
|
||||
// So this is its own router under the `chat` capability. Same grant, no owner gate, and nothing here reports
|
||||
// on anyone but the caller: two booleans about their own home. Denying it would not restrict an account, it
|
||||
// would just replace an explanation with a silence.
|
||||
//
|
||||
// ── Why the platform cannot fix `loggedIn` for them ──
|
||||
//
|
||||
// `claude` authenticates interactively against an Anthropic account, so logging in is the member's own act.
|
||||
// The alternative — pointing them at the owner's credential proxy — spends the owner's subscription on their
|
||||
// turns, which is the thing the whole per-user design exists to avoid. What the UI does with a `false` here is
|
||||
// render a terminal and tell them to run `claude` once themselves.
|
||||
|
||||
export const agentStatusRouter = createRouter();
|
||||
|
||||
type AgentStatus = {
|
||||
/** Their own `claude` is installed in their home. */
|
||||
installed: boolean;
|
||||
/** They have logged in, so a turn can actually run. */
|
||||
loggedIn: boolean;
|
||||
/** True for the owner, whose agent has always worked and who needs no instructions. */
|
||||
isOwner: boolean;
|
||||
/** Null unless something is missing — the one thing to tell them, already resolved to their case. */
|
||||
instruction: string | null;
|
||||
};
|
||||
|
||||
agentStatusRouter.get('/', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
|
||||
const resolved = await resolveHomeDir(user.id);
|
||||
if (!resolved.ok) {
|
||||
// No Linux account means nothing is confined and nothing can run — the same refusal the file browser
|
||||
// gives, reported rather than thrown because this endpoint exists to explain, not to gate.
|
||||
return ctx.json<AgentStatus>({
|
||||
installed: false,
|
||||
loggedIn: false,
|
||||
isOwner: false,
|
||||
instruction: resolved.reason,
|
||||
});
|
||||
}
|
||||
|
||||
// The owner runs in their real login home with the credential the proxy already holds. There is no member
|
||||
// account to `runAs`, and nothing to instruct.
|
||||
if (resolved.isOwner) {
|
||||
return ctx.json<AgentStatus>({ installed: true, loggedIn: true, isOwner: true, instruction: null });
|
||||
}
|
||||
|
||||
const row = await getUserById(user.id);
|
||||
if (!row?.osUser) {
|
||||
return ctx.json<AgentStatus>({
|
||||
installed: false,
|
||||
loggedIn: false,
|
||||
isOwner: false,
|
||||
instruction: 'This account has no Linux user on this machine yet — ask the server owner to provision it.',
|
||||
});
|
||||
}
|
||||
|
||||
const state = await claudeLoginState({ email: row.email, osUser: row.osUser });
|
||||
|
||||
const instruction = !state.installed
|
||||
? 'Claude is not installed in your home yet — ask the server owner to reprovision your account.'
|
||||
: !state.loggedIn
|
||||
? 'Open a terminal and run `claude` once to sign in with your own Anthropic account. It stays signed in.'
|
||||
: null;
|
||||
|
||||
return ctx.json<AgentStatus>({ ...state, isOwner: false, instruction });
|
||||
});
|
||||
Reference in New Issue
Block a user