insta tokens list/create/revoke; scoped-token login and project resolution - #291
Conversation
… resolution
New noun group `insta tokens` (spec 2026-09-23-scoped-api-tokens §9):
- `create <name>` binds to an org by default — `--project` (implies its
org), `--org`, the org an org-scoped login is bound to, the linked
project's org, or the caller's only org; several or none stops and asks
for `--org`/`--account`. `--account` is the explicit account-wide token
and is exclusive with `--org`/`--project`. `--read-only`, `--expires
30d|90d|1y|never` (default 90d, `never` omits expiresInDays). Human mode
prints the plaintext alone on stdout and the shown-once note on stderr;
`--json` is one `{ token, record }` document.
- `list` renders id, name, scope (account / org:<id8> / <id8>/<id8>),
access, expiry, last use, revocation; `revoke <id>` DELETEs /tokens/:id.
Scoped login (§6, §9.2):
- `login --api-key` stores `/me.token` as `GlobalConfig.tokenScope`
(validated shape; scrubbed with the session on a foreign control plane,
deleted when a plain key or a session replaces the scoped key).
- Implicit project resolution reads tokenScope first: a project token
never calls GET /orgs (403 under it) and builds the link from
GET /projects/:id, default branch from Branch.is_default; an org token
skips GET /orgs and lists the bound org. Extracted as
resolveProjectFromApi so it is testable over a fake fetch.
- The command guard renders a 403 `token_scope` as the platform message
plus one hint naming the stored scope — not as a permissions error.
- `insta status` and the api-key login line show the scope.
- `--mcp-token` keeps minting an account-wide token; its help says so.
Tests: test/tokens.test.ts, test/project-resolve-scoped.test.ts, and
login-api-key extended (DI fakes). help-surface pins the 26th visible
command; README and the dev skill's group table list `tokens`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 11 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/config.ts">
<violation number="1" location="src/config.ts:35">
P2: The `env use` switch drops the key but preserves this field, so `status` reports the old token scope on the new environment while logged out. Clear `tokenScope` with the credential.</violation>
</file>
<file name="src/commands/tokens.ts">
<violation number="1" location="src/commands/tokens.ts:41">
P2: The documented choices are only `30d | 90d | 1y | never`, but this regex accepts arbitrary positive durations and sends them to `/tokens`. Reject unsupported values as the usage error promises.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| user?: { id: string; email: string | null; name: string | null } | ||
| autoUpdate?: boolean // self-update on new releases (default true while pre-1.0) | ||
| agentCredential?: boolean // the stored insta_ key was minted by agent auth: the platform already treats it as an agent, so agent mode sends no enrollment evidence | ||
| tokenScope?: TokenScopeInfo // the stored insta_ key's binding (see TokenScopeInfo); belongs to the key, so it travels and is scrubbed with it |
There was a problem hiding this comment.
P2: The env use switch drops the key but preserves this field, so status reports the old token scope on the new environment while logged out. Clear tokenScope with the credential.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/config.ts, line 35:
<comment>The `env use` switch drops the key but preserves this field, so `status` reports the old token scope on the new environment while logged out. Clear `tokenScope` with the credential.</comment>
<file context>
@@ -18,13 +18,21 @@ const PROJECT_FILE = 'project.json'
user?: { id: string; email: string | null; name: string | null }
autoUpdate?: boolean // self-update on new releases (default true while pre-1.0)
agentCredential?: boolean // the stored insta_ key was minted by agent auth: the platform already treats it as an agent, so agent mode sends no enrollment evidence
+ tokenScope?: TokenScopeInfo // the stored insta_ key's binding (see TokenScopeInfo); belongs to the key, so it travels and is scrubbed with it
}
</file context>
There was a problem hiding this comment.
Fixed in bc5a4c9: env use now deletes tokenScope alongside the credential (src/commands/env.ts), with the env-switch tests asserting it is gone.
| if (v === undefined) return undefined | ||
| const s = v.trim().toLowerCase() | ||
| if (s === 'never') return undefined | ||
| const m = /^(\d+)([dy])$/.exec(s) |
There was a problem hiding this comment.
P2: The documented choices are only 30d | 90d | 1y | never, but this regex accepts arbitrary positive durations and sends them to /tokens. Reject unsupported values as the usage error promises.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/tokens.ts, line 41:
<comment>The documented choices are only `30d | 90d | 1y | never`, but this regex accepts arbitrary positive durations and sends them to `/tokens`. Reject unsupported values as the usage error promises.</comment>
<file context>
@@ -0,0 +1,146 @@
+ if (v === undefined) return undefined
+ const s = v.trim().toLowerCase()
+ if (s === 'never') return undefined
+ const m = /^(\d+)([dy])$/.exec(s)
+ const n = m ? Number(m[1]) : 0
+ if (!m || !Number.isSafeInteger(n) || n <= 0) throw new Error(`--expires expects 30d | 90d | 1y | never, got "${v}"`)
</file context>
There was a problem hiding this comment.
Resolved in bc5a4c9 by making the documentation honest rather than narrowing the parser: the platform accepts any day count (capped server-side), so <n>d / <n>y stay accepted and the --expires help text and the usage error now say so (30d / 90d / 1y are presets, not the only values).
…-scoped login cannot mint, honest --expires help Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jwfing
left a comment
There was a problem hiding this comment.
Summary
The supplied diff implements the stated token commands, credential-scope lifecycle, and scoped project resolution without a confirmed blocking defect. I found two non-blocking improvements. Review confidence is limited because the shell sandbox failed before execution, preventing checkout inspection and test execution.
Requirements context
Assessed against the PR description, supplied AGENTS.md instructions, and README changes in the diff. The referenced scoped-token design and plan were not available in the supplied material; filesystem access failed and the linked GitHub PRs could not be fetched. Their requirements and platform compatibility could not be independently verified.
Findings
Critical
(none)
Suggestion
- src/commands/tokens.ts: resolveBinding: Apply the project-token preflight consistently - The project-scoped credential check runs after the explicit --project and --org branches, while --account bypasses resolveBinding entirely. Those invocations therefore reach the API instead of receiving the dedicated local explanation. Move the check into tokensCreate before binding resolution and cover all explicit scope options. Platform enforcement still prevents unauthorized minting.
- test/tokens.test.ts: 403 token_scope rendering: Exercise scope errors through the command guard - These tests validate tokenScopeErrorLines directly, but do not exercise the new asynchronous onError path. Add a CLI-level test against a stub returning 403 token_scope to verify the platform message, stored-scope hint, nonzero exit status, and absence of unexpected stdout.
Information
- Functionality: Core behavior matches the supplied requirements - The diff implements default-org precedence, explicit account scope, expiry conversion, token-only stdout, JSON output, and scope-aware project resolution. Tests assert that project tokens avoid GET /orgs and select the reported default branch. Platform #538 must be deployed first as described; that dependency was not independently verified.
- Security: No confirmed security regression in the supplied diff - Scope metadata is cleared when credentials are replaced, cleared, or scrubbed for another control plane. Creation prints the secret only in the requested stdout result; its stderr note excludes it. Explicit project and revoke identifiers are URL-encoded, and no dependencies are added. Server-side authorization was outside the accessible material.
- Performance: No material performance issue identified - Binding resolution uses a bounded number of API requests, scoped resolution removes unnecessary org discovery, and listing adds no per-token requests. Table formatting uses memory proportional to the returned token list.
- Testing: Useful coverage present; execution unavailable - The supplied tests cover binding decisions, output separation, credential replacement, environment scrubbing, and resolution using an injected fetch transport. Shell commands failed with a bwrap namespace-permission error before execution, so surrounding conventions, the checked-out files, and reported green checks could not be independently verified.
Tests
No tests executed. Read-only shell inspection failed before execution because bwrap could not create a namespace. Reviewed the supplied diff and tests statically; the reported typecheck, 1848-test run, build, and smoke test remain unverified.
Verdict
approved: No Critical finding was established from the available evidence, so the strict verdict rule yields approved with non-blocking suggestions and explicit verification limits.
Spec: insta-cloud
docs/superpowers/specs/2026-09-23-scoped-api-tokens-design.md§9 · Plan Task 8. Platform side: https://github.com/InsForge/instacloud-platform/pull/538 (merge + deploy first — the new body fields and/me.tokencome from there).New noun group
insta tokenstokens list [--json]—id name scope access expires last-used(scopeaccount/org:<id8>/<org8>/<proj8>,revokedtrailing cell).tokens create <name> [--org <id> | --project <id> | --account] [--read-only] [--expires 30d|90d|1y|never] [--json]— binds to an org by default:--project→ its org;--org; an org-scoped login's own org; the linked project's org; the caller's only org; several orgs die asking for--orgor--account.--accountis the explicit account-wide choice and is mutually exclusive with--org/--project. Default expiry 90d. Human output: the plaintext ALONE on stdout ($(insta tokens create ci)is the token), the shown-once note on stderr;--jsonprints exactly one{ token, record }document.tokens revoke <id> [--json].Scoped login
login --api-keyrecords/me.tokenasGlobalConfig.tokenScope(belongs to the key: cleared when a plain key or a session replaces it; scrubbed with the credential on a foreign control plane).resolveProjectFromApi) reads it first: a project token never callsGET /orgs(which would 403) — the link is built fromGET /projects/:id(default branch frombranches[].is_default,mainfallback); an org token skips/orgsand lists its bound org.403 { error: 'token_scope' }is rendered as the platform message plus one hint naming the credential this login holds — deliberately not a "permission" message: the user is a member, the credential is narrow.insta statusshows the token scope;--mcp-tokenhelp now says "account-wide".Tests
test/tokens.test.ts(29: expires grammar, default-org cases, output shapes, token_scope rendering),test/project-resolve-scoped.test.ts(5: realApiClientover a fake fetch),test/login-api-key.test.ts(+3),test/help-surface.test.tsupdated (26 visible commands).npm run typecheck && npm test(88 files / 1848 tests) andnpm run buildgreen;login --api-key→status→org listsmoke-tested against a stub server with the built binary.README command table and the dev skill's group list updated. Release after merge; skills docs (insta-skills
feat/tokens-docs) wait for that release.🤖 Generated with Claude Code
Summary by cubic
Adds an
insta tokenscommand group (list, create, revoke) for scoped API tokens, and makeslogin --api-keyrecord a token's scope so implicit project resolution never calls routes the token can't reach.Tokens
tokens createbinds to an org by default (--org/--project);--accountis the exclusive account-wide choice.tokens createstops with a clear error instead of asking for an org token.--jsonprints one{ token, record }document.tokens listshows scope, access, expiry, last use, and revocation;tokens revoke <id>deletes the token.Scoped login and resolution
login --api-keystores/me.tokenas the credential's scope; a plain key or session replaces it, and env switches and foreign control planes scrub it./projects/:idand skipsGET /orgs, which would 403; an org token lists only its bound org.403 token_scoperenders the platform message plus a hint naming the held credential, not a permissions error.insta statusand the api-key login line surface the scope.Written for commit bc5a4c9. Summary will update on new commits.