@vectros-ai/cli
v0.21.0
Published
The Vectros provisioning CLI — `vectros bootstrap` mints a least-privilege MCP credential (ssk_* + AccessProfile + optional blueprint) without the root key. Owns the scope gate (the trust boundary).
Maintainers
Readme
@vectros-ai/cli
The Vectros provisioning CLI — the vectros binary:
vectros bootstrap— provision a least-privilege MCP credential (+ optional blueprint).vectros blueprint— author + inspect blueprints (init/validate/plan/list), creds-free.vectros blueprint-test— apply a blueprint to a live env, assert it provisioned a working credential, then tear it down.vectros context— create, list, and destroy app contexts.vectros issuers— register, update, delete, and inspect third-party IdP issuers (token-exchange trust anchors) for a context.vectros namespace— register a new scope namespace, and list/get existing ones.vectros identity— manage tenant-wide principals (users, orgs, clients, and namespaces you register).vectros role— define reusable, context-scoped access rules.vectros scripts— push, list, inspect, and delete the versioned script source a trigger fires, or your own app executes directly.vectros assume— switch identity namespace values on your own token, per a role'sassumablegrant.vectros access— bind principals to contexts, and inspect what a binding grants.vectros key— issue, list, revoke, and rotate scoped API keys (ssk_*).vectros keyring— hold several identities on one machine, pick which is active, and let other tools resolve keys from one place.vectros join— enroll a principal in a context and keep the resulting credential in the keyring, in one step.
npx -y @vectros-ai/cli bootstrap # provision a credential
npx -y @vectros-ai/cli blueprint --help # the authoring commands
npx -y @vectros-ai/cli keyring list # the identities on this machineEvery command has --help. The authoring loop is documented in
AUTHORING.md.
vectros bootstrap
Provisions a least-privilege Vectros credential for the
@vectros-ai/mcp-server
in one command — without your root key and without manual admin-app steps.
It:
- mints a scoped key (
ssk_*) for this machine (independently rotatable), - creates the matching AccessProfile — data-plane only,
- optionally scaffolds a use-case blueprint (e.g.
task-management: a schema + seed data), and - safe-merges the
vectrosserver entry into your MCP client config (claude_desktop_config.json— Claude Desktop, Cursor, Cline), backing up the original first. Pass--client codeto target Claude Code instead: it merges the project.mcp.jsonand prints the equivalentclaude mcp addcommand.
# Interactive — pick a blueprint (or a blank read-only credential), sign in once.
npx -y @vectros-ai/cli bootstrap
# Scripted / agent — token in the env, no prompts.
VECTROS_BOOTSTRAP_TOKEN=… npx -y @vectros-ai/cli bootstrap \
--blueprint task-management --yes
# Login once, then bootstrap unattended — no token needed; --yes reuses your
# stored `vectros login` session.
npx -y @vectros-ai/cli login
npx -y @vectros-ai/cli bootstrap --blueprint task-management --yes
# Provision a PRODUCTION context: the blueprint's schemas + key, but NONE of its
# illustrative sample records.
npx -y @vectros-ai/cli bootstrap --blueprint task-management --no-seed --yes
# See every flag.
npx -y @vectros-ai/cli bootstrap --helpRe-running is idempotent; --rotate revokes + re-mints just this machine's key.
On a machine with no local record of a prior mint (e.g. an ephemeral CI
container re-running --yes for the first time) against a blueprint whose
service principal already exists in the tenant, --yes alone isn't enough —
add --confirm-existing-principal to make that expected case explicit and
non-interactive; see AUTHORING.md's "idempotency contract" for why.
A bundled blueprint ships with sample seed records so the credential works
the moment it's minted. For a production context that's synthetic clutter:
--no-seed provisions everything else — the schemas, the AccessProfile and the
scoped key — but creates no sample records, leaving the context clean for real
data. (Interactively, a blueprint with seeds asks before adding them.) This
differs from --blank, which is credential-only and drops the schemas too.
vectros blueprint — authoring (creds-free)
Author and inspect blueprints without an account. You'll run these repeatedly,
so install once and drop the npx prefix (npm i -g @vectros-ai/cli, or prefix
each command with npx -y @vectros-ai/cli). The inner loop:
vectros blueprint init my-thing # scaffold a commented YAML starter
# …edit ./my-thing.blueprint.yaml…
vectros blueprint validate ./my-thing.blueprint.yaml # structural + scope-gate + lint
vectros blueprint plan ./my-thing.blueprint.yaml # preview what bootstrap would provision
vectros bootstrap --blueprint ./my-thing.blueprint.yaml # apply itinit— write a starter (--from <bundled>copies an exemplar;--out,--force).validate— structural parse → scope gate → linter. Accepts a file or a bundled name.plan— validate, then a terraform-style preview; mints nothing.list— the bundled library.
Full guide: AUTHORING.md. Format reference:
@vectros-ai/blueprints.
vectros blueprint-test — verify it actually provisions (creds-required)
validate/plan check a blueprint statically; blueprint-test proves it
end-to-end against a live environment. It applies the blueprint, asserts
the result (the app-context + each schema are readable back, and the freshly
minted ssk_* authenticates as a scoped key with the requested actions), then
tears down what it created — schemas, the AccessProfile, seed records, and
the key. Teardown is created-only: anything that already existed before the run
is left untouched, so it's safe to point at a shared staging tenant.
# Apply → assert → teardown against staging (sign in first, or pass --token).
vectros blueprint-test ./my-thing.blueprint.yaml --env staging
# Leave the provisioned graph in place to inspect it.
vectros blueprint-test task-management --keepExit 0 = applied and every assertion passed; 1 = an assertion failed or apply
errored; 2 = no credential, or the blueprint requested control-plane scope.
The app-context is intentionally not deleted (it's idempotent and reused).
vectros context — app contexts
A context is the namespace all data (records, documents, schemas, roles, access
profiles) lives in — the unit bootstrap/blueprint-test provision into.
vectros context manages contexts directly, outside of a blueprint.
vectros context create my-app --name "My App" # idempotent — re-running converges
vectros context list # every context in the tenant
vectros context list --mine # only contexts you hold an access profile in
vectros context get my-app
vectros context destroy my-app # irreversible — asks you to re-type the id| Subcommand | Flags | Notes |
|---|---|---|
| create <contextId> | --name <name> (defaults to the id), --tenant test\|live | Idempotent by contextId; re-running converges. |
| list | --mine, --tenant test\|live | Default lists every context in the tenant; --mine narrows to contexts you hold an active access profile in. |
| get <contextId> | --tenant test\|live | |
| destroy <contextId> | --force, --tenant test\|live | Irreversible. Permanently erases the context and ALL of its records, documents, folders, schemas, roles, and access profiles, and revokes its scoped keys up front. Interactively you must re-type the context id to confirm; --force skips the prompt and is required when stdin is not a TTY (scripts/CI). Reports status: purging — the teardown itself runs asynchronously. |
All four accept --json for machine-readable output, plus the common auth
flags (--base-url, --token).
vectros issuers — registered third-party IdP issuers
Manage the third-party identity-provider registrations a context trusts for
RFC 8693 token exchange — the
issuer/jwksUri/audience a blueprint's issuers[] entries register, plus
any self-signup policy attached to one. Everything a blueprint's issuers:
block can do is available standalone here too — adding a second IdP, or
editing one issuer's selfSignupPolicies after initial setup, no longer
requires hand-authoring a blueprint and re-running a full apply.
vectros issuers register --context my-app --issuer-id auth0-prod \
--issuer https://your-tenant.us.auth0.com/ \
--jwks-uri https://your-tenant.us.auth0.com/.well-known/jwks.json \
--audience https://api.your-app.example.com \
--captured-claim name --captured-claim phone_number
vectros issuers list --context my-app
vectros issuers get primary --context my-app
vectros issuers update primary --context my-app --status suspended
vectros issuers delete primary --context my-app # re-types the id to confirm| Subcommand | Flags | Notes |
|---|---|---|
| register | --context, --issuer-id, --issuer, --jwks-uri, --audience (all required); --sub-claim, --email-claim, --userinfo-uri, --self-signup-policy <type:roleId> (repeatable), --captured-claim <name> (repeatable), --tenant test\|live | Idempotent by --issuer-id within the tenant — a repeat returns the existing registration unchanged, not an error. --captured-claim names additional OIDC claims to capture beyond the email claim on every successful token exchange, stored as your tenant's golden identity-provider-asserted copy for that user; omit it to capture nothing beyond email. At most 20 names of at most 64 characters, no duplicates. |
| update <issuerId> | --context <contextId> (required); --sub-claim, --email-claim, --userinfo-uri, --status active\|suspended, --self-signup-policy <type:roleId> (repeatable), --clear-self-signup-policies, --captured-claim <name> (repeatable), --clear-captured-claims, --tenant test\|live | Omitting --captured-claim leaves the stored list UNCHANGED; --clear-captured-claims sends an explicit empty list (capture nothing beyond email). The two are mutually exclusive, and the same absent-vs-clear split applies to --self-signup-policy. Only ever sends the SAFE, mutable fields — issuer/jwks-uri/audience/contextId are trust-anchor fields, immutable via this route on the platform side; rotating one is delete + register. |
| delete <issuerId> | --context <contextId> (required), --force, --tenant test\|live | Re-types the issuer id to confirm (mirrors context destroy); --force skips the prompt and is required when stdin isn't a TTY. Refused server-side while any user was ever created or matched through this issuer. |
| list | --context <contextId> (required), --tenant test\|live | Every issuer registered in that context. --context has no default — an issuer read is confined to the context you name; there's no tenant-wide "every issuer in every context" view. |
| get <issuerId> | --context <contextId> (required), --tenant test\|live | One issuer's full registration, including its self-signup policies and captured claims. |
All five accept --json, plus the common auth flags (--base-url, --token).
vectros namespace — scope-namespace registrations
Register and inspect the namespaces a scope:<namespace> value can belong
to — whether values in that namespace must resolve to a real identity entity
(entityBacked, the same field identity create --type <namespace>
relies on) or are free-form strings. Registering a brand-new namespace (e.g.
a practice/team entity dimension) was blueprint-apply-only before this
command existed.
vectros namespace register practice --context my-app --specificity-rank 1500 --entity-backed
vectros namespace list # tenant-wide registrations
vectros namespace list --context my-app # just my-app's own
vectros namespace get practice --context my-app| Subcommand | Flags | Notes |
|---|---|---|
| register <name> | --context, --specificity-rank <n> (both required); --entity-backed, --default-schema-id <id> | NOT idempotent — a repeat name 400s (unlike issuers register). --context has no tenant-wide option here — a confined credential may only register against its own bound context. |
| list | --context <contextId> (optional) | Omit for every tenant-wide registration; supply for one context's own. |
| get <name> | --context <contextId> (optional) | Same two-mode read as list. |
No update/delete — both routes require a genuine root API key on the
platform side, which this CLI never holds, so neither could ever succeed
here. All three subcommands accept --json.
vectros identity — tenant-wide principals
Manage the principals a context's access profiles and roles reference: users
(the one fixed principal surface, --type user) and namespaced identity
entities (org, client, or any namespace you registered, via --type
<namespace>). Identities are tenant-wide and orthogonal to context — a
blueprint or role references one by id, it never creates one.
vectros identity create --type user --external-id [email protected] --email [email protected]
vectros identity create --type org --external-id acme-inc --name "Acme Inc"
vectros identity list --type user
vectros identity get --type org --id <vectrosId>
vectros identity delete --type org --id <vectrosId>
vectros identity invite --context my-app --role editor --email [email protected] \
--accept-url https://my-app.example.com/accept| Subcommand | Flags | Notes |
|---|---|---|
| create | --type <user\|namespace> (required), --external-id <id> (required — your stable idempotency key) | Idempotent, upsert-by-externalId. |
| | --type user only: --email <email>, --service (mint a SERVICE principal; default HUMAN) | --name/--scope are rejected for --type user. |
| | entity namespaces only: --name <name>, --scope <ns:value> (repeatable, up to 2 parent ownership edges) | --email/--service are rejected for an entity namespace. |
| | both: --metadata <json> | Free-form JSON object. |
| list | --type <user\|namespace> (required), --external-id <id> (filter), --limit <n> | Scoped to your own context — see list-all below for a user created but never granted access anywhere. Omit --limit and the CLI drains every page for the complete set; pass --limit (1–100) and it returns exactly one page of that size instead. |
| list-all | --external-id <id> (filter), --tenant test\|live, --limit <n> | Users only, and requires an account owner login. Lists every user in the tenant, regardless of app-context access — for recovering a user identity list can never show you: one created but not (yet) granted access anywhere, so no context-scoped view can ever reach it, and a create retry with the same --external-id just collides with the same invisible row. Find it here by --external-id, then grant it access or identity delete it with an ordinary credential. |
| get | --type <user\|namespace> (required), --id <vectrosId> (required) | --id is the Vectros-assigned id from create/list, not the --external-id. |
| delete | --type <user\|namespace> (required), --id <vectrosId> (required) | |
| invite | --context <c>, --role <roleId>, --email <e> (all required); --first-name <n>, --accept-url <url>, --no-send-email | Onboards a person into a context by email (POST /v1/users/invite) — no root key required, your vectros login session already carries enough scope. --role is an existing Role's own id (no separate lookup). --accept-url is required unless --no-send-email is given, in which case the response carries the raw inviteToken/acceptLink for you to deliver yourself. |
create/list/list-all/get/invite accept --json; delete does not.
vectros role — reusable, context-scoped access rules
A Role is a reusable, identity-agnostic scope rule bound to a principal later
via vectros access grant --role <roleId> — the same rule can be shared
across many principals. role create builds a single-clause role from a
flat --actions list; author a multi-clause role (several rules unioned
together) in a blueprint's roles: block instead.
vectros role create --context my-app --role-id editor --name Editor \
--actions records:cru,search:r
vectros role create --context my-app --role-id own-org --name "Own org" \
--actions records:r --data-scope '{"scope:org":["<orgId>", null]}'
vectros role create --context my-app --role-id hr-admin --name "HR Admin" \
--actions records:cru --assumable '{"scope:org":["org_engineering", "org_sales"]}'
vectros role create --context my-app --role-id delegator --name Delegator \
--actions profiles:c --assignable-role editor --assignable-role own-org
vectros role list --context my-app
vectros role get --context my-app --role-id editor
vectros role delete --context my-app --role-id editor| Subcommand | Flags | Notes |
|---|---|---|
| create | --context <c>, --role-id <id>, --name <n>, --actions <csv> (all required); --data-scope <json>, --assumable <json>, --assignable-role <roleId> (repeatable), --description <text>, --tenant test\|live | Idempotent by roleId. --assignable-role restricts WHICH named roles this clause may compose into a delegated AccessProfile — orthogonal to --data-scope, which bounds data reach rather than role composition. Omit it for no restriction; the platform rejects an empty list, so there is no way to spell "compose nothing". Once a clause carries the restriction it can compose only the roles it names, so name every roleId it still needs before adding the flag. access explain reports the restriction for clauses that carry one. --data-scope confines the clause to owned data — a JSON map of ownership dimension → allowed values, ANDed across keys (e.g. {"userId":["${{ self.userId }}"]}, or {"scope:org":["<orgId>", null]} — the null entry additionally admits scope-less/tenant-shared records). Keys must be userId or scope:<namespace> (the legacy flat orgId/clientId keys are rejected server-side). --assumable is a SEPARATE, role-level grant — a JSON map of scope:<namespace> → allowed values naming which values a holder of this role may switch into via vectros assume; holding broad --data-scope reach in a namespace does not by itself grant assuming any value in it. |
| list | --context <c> (required); --tenant test\|live | Add --tenant test to read a context that lives in the test tenant — a blueprint applied there is otherwise unreadable. |
| get | --context <c>, --role-id <id> (both required); --tenant test\|live | Also shows the role's assumable grant, if any is set. |
| delete | --context <c>, --role-id <id> (both required); --tenant test\|live | No cascade — any access profile still referencing the role fails to mint (not silently downgraded) until reassigned. |
create/list/get accept --json; delete does not.
vectros scripts — the script source a trigger fires, or your app runs directly
A stored script has two consumers, and this is how the source gets there for both. A trigger rule
names it in scriptRef and runs it under the trigger's own grant; that scriptRef is validated when
the rule is DECLARED rather than when it fires, so a script has to exist before any rule can name it.
Your own app runs a stored version directly through POST /v1/scripts/execute, under its own
credential, committing everything the script wrote as a single transaction.
Direct execution is separately granted: the calling role needs scripts:x, and without it the call
is a 403 that cannot say the missing grant is the reason. The platform accepts it bare (every
script in the context) or qualified to one name (scripts:x:create-case). A blueprint may use
only the qualified form — its scope gate refuses a bare scripts:x and there is no wildcard — so a
blueprint that ships a script grants exactly the one it ships. See The scope gate below, and
AUTHORING.md for where that grant goes in a blueprint.
vectros scripts push notify-assignee --context my-app --source-file ./notify.js
vectros scripts push notify-assignee --context my-app --source-file ./notify.js --if-changed
vectros scripts list --context my-app # every script, newest first
vectros scripts list --context my-app --name notify-assignee # every version of one name
vectros scripts get 3b1c9f2e7a5d… --context my-app # one version, with its source
vectros scripts delete 3b1c9f2e7a5d… --context my-app # ONE version (prompts)
vectros scripts delete 3b1c9f2e7a5d… --context my-app --force # ...or skip the prompt, e.g. in CI| Subcommand | Flags | Notes |
|---|---|---|
| push | --context <c>, --source-file <path> (both required); --input-contract <text> or --input-contract-file <path>, --if-changed, --tenant <test\|live> | Always stores a new version by default — pushing is not idempotent, and each push gets the next server-assigned version number, which the output reports. --if-changed reads the current version first and pushes only on a genuine difference; use it where the same push re-runs, e.g. in CI. --source-file is capped at 300,000 UTF-8 bytes and the input contract at 50,000, both checked locally so an oversized push fails immediately instead of sending the body. The input contract is descriptive only — nothing validates a firing against it. Nothing parses the source either, so a syntax error surfaces the first time the version runs. |
| list | --context <c> (required); --name <name>, --tenant <test\|live> | With --name, every version of that name, oldest first; without, every script in the context, newest first. The human view shows a byte count instead of the source — a listing would otherwise print the full text of every version. --json includes the source. |
| get | --context <c> (required) + the version id; --tenant <test\|live> | Shows one version, including its full source. Version ids are opaque — a deterministic hex string, not a prefixed or random id — so copy them from scripts list. |
| delete | --context <c> (required) + the version id; --force, --tenant <test\|live> | Deletes ONE version; other versions of the same name are untouched. Irreversible — the stored source is the only copy — so it asks you to type the version id back before proceeding. --force skips that, and is REQUIRED in a non-interactive shell rather than the prompt being silently waived there. --json emits {id, deleted} on stdout; the warning always goes to stderr, so it stays visible without breaking a pipe. ⚠️ Deleting a name's LATEST version reuses its identity — the next push of that name recomputes the same version number and the same id, so a pinned reference silently resolves to different source. |
There is no scripts update — a stored version is immutable, and the platform's update route always
rejects. Supersede a version by pushing a new one.
--context is required on all four. Scripts are context-scoped and a trigger resolves its
scriptRef inside its own context, so a defaulted context would push where nobody asked and leave
the rule that wanted it still unresolvable. --tenant defaults to live; pass --tenant test to
reach the versions a blueprint-test --tenant test run created.
⚠️ delete is not reference-checked. Nothing verifies whether a trigger rule still points at the
version being removed, and the two outcomes differ: a rule pinned to that version fails on every
subsequent firing, while a rule on version: latest silently resolves to the PREVIOUS version and
carries on running older code with nothing to indicate it. This CLI has no verb that lists trigger
rules, so it cannot answer that question for you — check against the API before removing a version a
rule may still use.
A blueprint can ship a script instead of pushing one — see scripts: in
@vectros-ai/blueprints. An apply is
convergent (an unchanged source pushes nothing) and never deletes: removing a scripts: entry
leaves every version in place, which is why this command group exists to remove one deliberately.
vectros assume — switch identity namespace values on your own token
Re-mints your own verb token with one or more identity.<namespace> values
switched — the "org switcher" flow, named (like the platform endpoint it
calls) after AWS STS AssumeRole. Only works if a role you currently hold
explicitly grants assuming the requested value(s) via that role's own
--assumable grant (checked live against your CURRENT roles, never a value
frozen into your token at mint time).
vectros assume --context my-app --scope org:org_sales| Flags | Notes |
|---|---|
| --context <c> (required), --scope <ns:value> (repeatable, at least one required) | Each --scope becomes one scope:<namespace> entry in the request. No artificial cap on how many namespaces you name — the platform requires a SINGLE role to jointly grant the whole combination and rejects an unsatisfiable one itself. |
Prints the re-minted access_token/expires_in, plus the resolved identity and allowedActions the platform returns alongside it — what you actually assumed, without decoding the token (accepts --json) — there
is no --to-keyring here, since the result is an ephemeral bearer token
with the same remaining lifetime as the verb token this command itself
mints to make the call, not a durable secret the keyring is built to hold.
vectros access — bind principals to contexts
The binding layer between an identity and a context: access grant joins a
principal to a context via a Role reference (--role) or inline actions
(--actions) — never both. A later vectros key issue mints a credential
that authenticates as the bound principal and can never exceed this
binding.
vectros access grant --principal me --context my-app --role editor
vectros access grant --principal usr_<id> --context my-app --actions records:r,search:r \
--scope org:<orgId>
vectros access list --context my-app # who's in this context
vectros access list --principal me # which contexts I'm bound to
vectros access get --principal me --context my-app
vectros access explain --principal me --context my-app
vectros access revoke --principal usr_<id> --context my-app--principal accepts usr_<userId>, key_<keyId>, or the alias me (your
own signed-in user, resolved from your login) — every access subcommand
shares this grammar.
| Subcommand | Flags | Notes |
|---|---|---|
| grant | --principal <p>, --context <c> (required); exactly one of --role <roleId> (repeatable — 2+ compose additively) / --actions <csv>; --scope <ns:value> (repeatable), --identity-overrides <json>, --tenant test\|live | Idempotent upsert — re-granting an existing binding updates it to the newly-declared shape (role(s)/actions/overrides), it does not no-op. Repeating --role composes: --role case-handler --role hr-admin grants each role's own clauses, concatenated in the order given — never merged, so each clause keeps meaning exactly what its own author wrote. --scope/--identity-overrides extend the principal's stamped identity (e.g. --scope org:<id> → scope:org) — a credential can only stamp ownership its identity carries, so granting write access to org-scoped data requires that scope on the profile. When both are given, --identity-overrides wins on key conflicts. |
| revoke | --principal <p>, --context <c> (required); --tenant test\|live | |
| list | Exactly one of --context <c> / --principal <p>; --tenant test\|live | --context lists the context's members; --principal lists every context that principal is bound to. |
| get | --principal <p>, --context <c> (required); --tenant test\|live | |
| explain | --principal <p>, --context <c> (required); --tenant test\|live | Resolves the binding's clauses (following every referenced role and concatenating their clauses, if bound via one or more) and renders a concrete access matrix — a canned set of record-ownership archetypes (yours alone, another user's, org-shared, scope-less, …) evaluated against the clauses. A client-side preview for authoring-time sanity, not the platform's own evaluator: it models READ reachability only — a c/u/d action shown as granted is advisory, not a guarantee a specific write will succeed, since the platform's write rule (a clause must explicitly name a dimension to place a value into it) diverges from its read rule (an unmentioned dimension never narrows reads). |
grant/list/get/explain accept --json; revoke does not.
vectros join — enroll and keep the credential, in one step
join is a composition verb: everything it does already exists as a
discrete command (access grant, key issue, keyring add) — join runs
them in order against one context and records the outcome as a keyring
entry, so vectros switch <alias> (and the MCP server / recall hook) can act
as that principal from then on.
vectros join my-app --role editor # enroll "me" via a role
vectros join my-app --role case-handler --role hr-admin # compose 2+ roles additively
vectros join my-app --actions records:cru,search:r --scope org:<orgId>
vectros join my-app --as usr_<id> --role editor --alias teammate| Flag | Notes |
|---|---|
| --role <roleId> | Repeatable — 2+ compose additively, same semantics as access grant --role. XOR with --actions. |
| --actions <csv> | Inline single-clause grant. XOR with --role. |
| --as me\|usr_<id> (default me) | Which principal to enroll — me resolves to your own signed-in user. |
| --scope <ns:value> | Repeatable. Places the member under a scope for this context (an identity override on the profile) so their key can WRITE that scope's data. |
| --alias <name> | Keyring entry name (defaults to the context id). |
| --activate / --no-activate | Force (or skip) making the freshly-stored key this machine's active identity. |
Re-joining an alias whose secret is still held locally is idempotent: it re-grants (a no-op upsert) and re-activates without churning the key. Only when the local secret is missing does it rotate to obtain a fresh one.
vectros key — the scoped API key (ssk_*) lifecycle
Mint, inspect, revoke, and rotate the scoped keys (ssk_*) that authenticate
as a principal already bound to a context (vectros access grant first — a
key can never exceed its profile). The raw secret is disclosed exactly
once, on issue/rotate; if you lose it, key rotate mints a fresh one.
vectros key issue --principal usr_<id> --context my-app # print the secret (default)
vectros key issue --principal usr_<id> --context my-app --to-keyring # store it instead
vectros key list # keys in the default context
vectros key list --all-contexts # every key, every context (owner-only)
vectros key get <keyId>
vectros key revoke <keyId>
vectros key rotate --principal usr_<id> --context my-app # revoke + re-mint the same tuple--principal here is usr_<userId> or a bare user id (the leading usr_
is optional and stripped if present) — narrower than access's grammar: there
is no me alias on any key subcommand, and a key never authenticates as a
key_* principal. Look your own id up first with vectros identity list
--type user (or vectros whoami), or grant/issue via vectros join, which
does resolve --as me.
A minted secret has exactly two destinations, and the flags for choosing one
are shared by issue and rotate:
| Flag | Meaning |
|---|---|
| --format human\|raw\|env\|json (default human) | Print the secret — raw = bare secret only, env = a VECTROS_API_KEY=... line, json = the full key object. |
| --to-keyring | Store the secret in this machine's keyring instead of printing it (the desktop/MCP path). Mutually exclusive with an explicit --format raw\|env\|json. |
| --alias <name> | Keyring entry name for --to-keyring (defaults to the context id). |
| --no-activate | With --to-keyring, store the entry without making it the active identity. |
| Subcommand | Flags | Notes |
|---|---|---|
| issue | --principal <p>, --context <c> (required); --name <n> (default default), --label <l>, --tenant test\|live, destination flags above | Idempotent on (tenant, context, principal, name) — re-issuing an existing tuple returns the key without the secret (never re-disclosed); the CLI detects that and points you at rotate. |
| list | --principal <p>, --context <c>, --tenant test\|live or --all-contexts (owner-only; spans both tenants, not combinable with --tenant) | The default lists only ONE context's keys (the token's context — default, or --context), not your whole account — easy to miss since the active-key limit is tenant-wide across all contexts. --all-contexts is the true account-wide view. |
| get <keyId> | --context <c>, --tenant test\|live | --context pins the verb token to the key's context — omit it for a key bound to a non-default context (e.g. one bootstrap minted) and the lookup 404s. |
| revoke <keyId> | --context <c>, --tenant test\|live | Soft-delete; ~5 minutes to take effect (authorizer cache). Same --context pinning caveat as get. |
| rotate | --principal <p>, --context <c> (required); --name <n>, --force-new, --tenant test\|live, destination flags above | Revokes the matching active key (by the exact (context, principal, name) tuple), then mints a fresh one — there is no dedicated rotate endpoint. Default-name guard: if no key is currently active under --name (default default) but the principal/context has other active keys, rotate refuses rather than silently minting an unrelated second key — pass the right --name or --force-new to proceed anyway. |
issue/rotate have no --json (use --format json instead); list/get
accept --json, revoke does not.
vectros keyring — one home for your keys
A machine can hold several Vectros identities (one per context or principal).
The keyring stores them as named entries plus one active pointer, so you can
switch between them without re-issuing keys. vectros join <context> adds an
entry and activates it; vectros key issue/key rotate add one when passed
--to-keyring (which stores the new secret instead of printing it); vectros switch
<alias> flips the active one.
vectros keyring list # the entries; * marks the active one
vectros switch <alias> # make <alias> active (= keyring use)
vectros keyring show # the active entry's fields + its key
vectros keyring show --format raw # JUST the secret, for scripts (see below)
vectros keyring doctor # is the keyring healthy? which keys are live?
vectros keyring prune # drop entries whose stored secret is gone
vectros keyring remove <alias> # drop one entry + its secretThese are local — no network, no auth. The index itself never holds a secret.
Where your keys are kept. In your OS credential store (Windows Credential
Manager, Linux Secret Service) where one is available, so they are protected by
your OS login. Where none is — a headless Linux box with no Secret Service, or a
platform with no prebuilt for the optional native package — the CLI falls back to
an encrypted file under ~/.vectros/secrets rather than failing. Be aware of what
that fallback does and doesn't buy you: it protects a single stray .enc file, but
its encryption key is derived from material stored alongside it, so anyone with a
copy of your whole ~/.vectros directory can decrypt it. Treat that directory
as a secret in its own right. vectros keyring doctor tells you which store you
are on and why; VECTROS_KEYRING_BACKEND=file forces the fallback (existing keychain keys keep
working and move back to the file as you use them).
On macOS the encrypted file is used for now. A Keychain item grants access to
the application that created it and prompts for anything else — and the
"application" here is the node binary running this CLI, which is not stable
across installs. We expect that to put a password prompt in the startup path of
any tool resolving a key unattended, which on a headless host nobody can answer.
We have not yet verified it on real hardware, and would rather ship the honest
fallback than a claim we haven't tested.
Upgrading from an older CLI needs no action: an existing encrypted secret moves into the credential store the first time that key is used.
Resolving a key from another tool. keyring show --format raw prints the bare
secret and nothing else, which makes it a credential helper in the shape of
git credential / docker-credential-* / aws credential_process:
export VECTROS_API_KEY=$(vectros keyring show --format raw) # active
export VECTROS_API_KEY=$(vectros keyring show --format raw --alias support-bot) # a specific entry@vectros-ai/mcp-server
does this for you: leave VECTROS_API_KEY unset and it resolves the key from
this keyring, so you don't paste a copy into its config. Exit codes: 0 secret
printed, 2 no such (or no active) entry, 1 the entry exists but its secret
can't be read.
When something's off, vectros keyring doctor is the first stop — it shows
which entry is active, whether each secret is readable, which entries are live
keys (the ones that act on real data), and which store is holding them, exiting
non-zero when it finds something to fix.
vectros keyring prune drops entries whose secret is gone, but only where "gone"
is knowable — that is, on the encrypted-file store. Entries kept in the OS
credential store are never pruned. That store can't tell us "no such secret"
apart from "couldn't read it right now", so a locked keychain would otherwise
look like a keyring full of dead entries; deleting one would throw away the key
id you need to revoke a key that may still be live. Use
vectros keyring remove <alias> to drop one deliberately.
The scope gate (the trust boundary)
Blueprints are untrusted input (agent- or community-authored) and the
CLI runs under an elevated ephemeral bridge token. So the trust boundary is
this binary, not the blueprint: a hardcoded scope gate bounds every
mint to a data-plane allowlist (records / schemas / search / documents
/ folders / inference / entities). Most control-plane scope (keys /
app-contexts / users / billing / admin / namespaces / triggers) or a
* wildcard hard-rejects — the CLI mints nothing and exits non-zero. There is
intentionally no override flag.
Two narrow, deliberate exceptions. The first: profiles (which configures who has
access to what — control-plane by nature) admits its r/u/d operations,
because the platform enforces an unconditional check that a write to an
AccessProfile can never grant more scope than the requesting credential
already holds — those verbs can only disclose or redistribute access, never
manufacture new reach. profiles:c (creating a profile for an arbitrary,
caller-chosen principal name) still hard-rejects — except profiles:c:self,
which the platform resolves from the credential's own identity at request
time, never from anything the blueprint declares, and except a bare
profiles:c on a clause that also declares the member-lifecycle capability
(see Capabilities, below) — that combination is admitted because
member-lifecycle already grants unbounded principal-creation reach, so
gating bare profiles:c behind it adds no further technical bound but keeps
the same explicit, fail-closed opt-in signal every other admission here
requires. A literal-qualified profiles:c:usr_<id> naming another principal
still hard-rejects regardless of member-lifecycle, same as every other
control-plane scope.
The second exception: scripts:x:<script-name> — execute one named script — is admitted, so a
blueprint that ships a script in its scripts: block can also grant permission to run it. Execution
consults no data scope at all, and every write the script makes is still checked one by one against
the caller's own permissions, so this widens what can be composed, never what data is reachable.
Everything else on the resource still hard-rejects: a bare scripts:x, because it would mean "run
every script in this context, including ones added later that your reviewer never saw"; scripts:x:*,
because there is no wildcard form — the qualifier is compared to the script name exactly, so such
a grant would store cleanly and then refuse every execution; and scripts:c, authoring, which stays
a design-time admin surface. triggers is unchanged.
Capabilities (granted_capabilities) — a separate, equally-gated dimension
A blueprint's accessProfile and role clauses may also declare
capabilities: [...] — named platform effects (like creating or removing an
identity in your tenant) that reach across a partition boundary in a way no
resource:ops verb can express. This rides a field entirely separate from
allowedActions, and it has its own allowlist: member-lifecycle and
delegate-principal-stamp are admitted today. Every other capability name —
including ones the platform's own registry recognizes — is refused, the same
fail-closed, deliberately-widen posture as the scope gate above. vectros blueprint validate/plan and
bootstrap all report a rejected capability the same way they report a
rejected scope: nothing mints, and the CLI exits non-zero.
Rate limits
blueprint-test provisions against the live API, so a large blueprint can brush the per-account
per-minute rate limit. The CLI surfaces the 429 (with its Retry-After hint) directly — rerun
after the window resets, or split a large blueprint. See the
rate limits guide.
See also
@vectros-ai/mcp-server— the runtime this CLI provisions for.@vectros-ai/blueprints— the curated use-case library the CLI applies.
Security & trust
Vectros enforces per-customer, fail-closed isolation and least-privilege scoped keys, with a tamper-evident audit and version history. Customer-facing surfaces are hardened through extensive adversarial security review. For the full trust posture, drawn plainly with its boundaries, see the compliance and trust guide.
License
Apache-2.0. See the LICENSE file.
