Skip to content

insta tokens list/create/revoke; scoped-token login and project resolution - #291

Merged
jwfing merged 2 commits into
mainfrom
feat/tokens-command
Sep 24, 2026
Merged

jwfing merged 2 commits into
mainfrom
feat/tokens-command

Conversation

@jwfing

@jwfing jwfing commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

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.token come from there).

New noun group insta tokens

  • tokens list [--json] — id name scope access expires last-used (scope account / org:<id8> / <org8>/<proj8>, revoked trailing 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 --org or --account. --account is 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; --json prints exactly one { token, record } document.
  • tokens revoke <id> [--json].

Scoped login

  • login --api-key records /me.token as GlobalConfig.tokenScope (belongs to the key: cleared when a plain key or a session replaces it; scrubbed with the credential on a foreign control plane).
  • Implicit project resolution (resolveProjectFromApi) reads it first: a project token never calls GET /orgs (which would 403) — the link is built from GET /projects/:id (default branch from branches[].is_default, main fallback); an org token skips /orgs and lists its bound org.
  • A 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 status shows the token scope; --mcp-token help 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: real ApiClient over a fake fetch), test/login-api-key.test.ts (+3), test/help-surface.test.ts updated (26 visible commands). npm run typecheck && npm test (88 files / 1848 tests) and npm run build green; login --api-key → status → org list smoke-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 tokens command group (list, create, revoke) for scoped API tokens, and makes login --api-key record a token's scope so implicit project resolution never calls routes the token can't reach.

Tokens

  • tokens create binds to an org by default (--org/--project); --account is the exclusive account-wide choice.
  • A project-scoped login can't mint: tokens create stops with a clear error instead of asking for an org token.
  • The plaintext token prints alone on stdout; --json prints one { token, record } document.
  • tokens list shows scope, access, expiry, last use, and revocation; tokens revoke <id> deletes the token.

Scoped login and resolution

  • login --api-key stores /me.token as the credential's scope; a plain key or session replaces it, and env switches and foreign control planes scrub it.
  • Project resolution reads the scope first: a project token resolves directly from /projects/:id and skips GET /orgs, which would 403; an org token lists only its bound org.
  • A platform 403 token_scope renders the platform message plus a hint naming the held credential, not a permissions error.
  • insta status and the api-key login line surface the scope.

Written for commit bc5a4c9. Summary will update on new commits.

Review in cubic

… 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>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/config.ts
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bc5a4c9: env use now deletes tokenScope alongside the credential (src/commands/env.ts), with the env-switch tests asserting it is gone.

Comment thread src/commands/tokens.ts
if (v === undefined) return undefined
const s = v.trim().toLowerCase()
if (s === 'never') return undefined
const m = /^(\d+)([dy])$/.exec(s)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread src/commands/tokens.ts
…-scoped login cannot mint, honest --expires help

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Fermionic-Lyu Fermionic-Lyu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, Approved.

@jwfing
jwfing merged commit cc6fd08 into main Sep 24, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants