The auditor's cron is live, but the auditor itself is optional — and that combination is
safe. Eve creates one Vercel Cron Job per file inagent/schedules/, so a schedule's
location is the on/off switch; enabling or withdrawing it is agit mvplus an operator
decision, never a side effect of a merge. A deployment that sets no auditor credential
no-ops the tick with a log line instead of failing, so you can run the steward with no
auditor at all and get a quiet, green cron. A deployment that sets a partial credential
still fails loudly — that is a misconfiguration, not an absence.
bootstrap-team-self-cognition.md.package.json, npm lockfile, and TypeScript toolchain. The canonical deployment isworkspaces member, so itvercel.json is the manifest inlined below.temper-ts) as an npm file: dependency that lives outside its root{
"$schema": "https://openapi.vercel.sh/vercel.json",
"installCommand": "npm install",
"ignoreCommand": "sh \"$(git rev-parse --show-toplevel)/scripts/vercel-ignore-build.sh\" steward-agent",
"buildCommand": "npm run build"
}prebuildbuild:dep, which installs and builds temper-ts before the agent compiles, soTEMPER_MCP_URL is required, thrown by the connection'srequireEnv guard).TEMPER_M2M_* orSTEWARD_MODEL value in the dashboard does nothing until the next deployment. A cron runningscripts/vercel-ignore-build.sh)packages/agent-workflows/steward/, clients/temper-ts/, clients/temper-telemetry-ts/, orscripts/vercel-*.sh; a doc change under the agent's own tree counts).Do not run eve deploy/vercel deployfrom inside the agent directory. The CLI
uploads only the directory it is invoked from, and the siblingtemper-tsdependency lives
outside the root directory — a CLI deploy launched from the agent directory cannot carry
it, and the build breaks on an unresolvable import. The git path clones the whole repo and
honors "include source files outside the Root Directory". Deploy by pushing.
.vercel/project.json) for vercel env pull and eve dev.link picker rather than eve link: eve link enumerates all youreve link hits the personalvercel env pull writes the AI Gateway key into .env.local for localeve dev. You do not need eve link at all.| steward (root agent) | citation auditor (declared subagent) | |
|---|---|---|
| Credential | TEMPER_M2M_* / TEMPER_CONNECT_CONNECTOR / TEMPER_TOKEN | TEMPER_AUDITOR_M2M_* / TEMPER_AUDITOR_TOKEN — no Connect |
| Model | STEWARD_MODEL (default minimax/minimax-m3) | AUDITOR_MODEL (default anthropic/claude-haiku-4.5) |
| Cogmap reach | --cogmap <ref> — write, it authors into its map | --cogmap <ref>:ro — read-only, or omitted entirely |
kb_events.emitter_entity_id. A shared client idAuditAuthority::Author, whoseAUDITOR_MODEL_FALLBACKS="" if you would rather the tick fail than collapse the personas.--cogmap grant classifies the auditor asAuditAuthority::Author for every finding in that map via can_modify_resource, and:ro, or nothing. See machine-token-contract.md §C.watcher isConnect is not an option for the auditor, structurally. A Vercel Connect connector is
deployment-scoped, and both agents share one deployment — so a Connect token would
authenticate the auditor as the steward, which is precisely the collapse above. The
auditor's fetch helper therefore offers only M2M and a static dev token.
resolve_machine_from_claims (the single machine entrykb_machine_clients or a 401.issue requires an instance whose AS is Temper's own. An instance has exactly oneissuedormant: true. Use provision there; it requires a--owner-team, which records the--team membership) and write on the map(s) it tends. The--team membership — and--owner-team — the team whose owners manage the machine. Read and write reach are declared--team and --cogmap, at registration: the principal is born with exactlyteam add-member,cogmap grant).temper team add-member andtemper cogmap grant --to-profile <agent-profile-id> --write — the profile already exists,--write.has_system_access reads kb_principal_standing alone,denied — reach does not clear it, and neitherprovision nor issue does.kb_machine_clients looks right, and every request returns 403 SYSTEM_ACCESS_REQUIRED. Ifrebind --no-revoke-old points it at the existing agentprovision a new one with the full owner team and reach instead.*_M2M_CLIENT_ID / *_M2M_CLIENT_SECRET env and get a deploymentvw_agent_exercise's rungs advance on theadmin machine revoke the old client. Revocation denies that client's authentication;Auth0-fronted (AUTH0_*) | Temper-as-AS (AS_ISSUER set) | |
|---|---|---|
| Who mints the secret | Auth0 — you create the M2M app | Temper — machine issue, printed once |
| Register with | temper admin machine provision --client-id <auth0-id> | temper admin machine issue (registers as it mints) |
| Client id shape | Auth0's opaque id | tmpr_… |
*_M2M_AUDIENCE | required — must equal the API's AUTH_AUDIENCE | omit it — the AS ignores a request-supplied audience and mints with its server-side AS_AUDIENCE |
*_M2M_TOKEN_URL | https://<tenant>.auth0.com/oauth/token | https://<instance>/oauth/token |
| Mint body encoding | form-encoded or JSON (Auth0 tolerates both) | form-encoded only |
Admission (access approve) | required | required |
kb_machine_clients registration | required | required |
Auth0 is the more permissive issuer, so it hides AS-mode defects. Both rows above where
the variants differ in tolerance rather than value are real bugs that stayed green for
as long as Auth0 was the only issuer any client faced. The JSON-vs-form one is documented incredentials.ts: RFC 6749 §4 mandates form encoding, Auth0 tolerates JSON, and Temper's AS
reads the body withreq.formData()— so a JSON mint never reaches its grant branch and
fails only on AS. Anything verified only against Auth0 is verified against the lenient
case. UseClientCredentialsfromtemper-tsrather than hand-rolling a mint; it is the
one implementation that is correct against both.
vercel env add <NAME>) before deploying.| Variable | Required | Value / purpose |
|---|---|---|
TEMPER_MCP_URL | yes | The temper-mcp endpoint, e.g. https://<instance>/mcp. The agent's sole model-facing seam to Temper. Points at your instance by this value alone. |
TEMPER_API_URL | yes | The temper REST base, e.g. https://<instance>. Distinct from TEMPER_MCP_URL; used by the code schedules' direct POST /api/steward/dispatch, GET /api/steward/candidates, and POST /api/cognitive-maps/{id}/materialize. |
TEMPER_M2M_CLIENT_ID | prod | The machine client id — the Auth0 M2M app's id, or the tmpr_… id from machine issue. When set, the agent mints its own client_credentials token and this strategy wins over Connect and TEMPER_TOKEN. |
TEMPER_M2M_CLIENT_SECRET | prod | The client secret. A Vercel env var only — never in code, never seen by the model. |
TEMPER_M2M_TOKEN_URL | prod | The issuer's token endpoint: https://<tenant>.auth0.com/oauth/token for provision, or your own instance's https://<instance>/oauth/token for issue. |
TEMPER_M2M_AUDIENCE | only for an external IdP | The API audience the minted token targets (must equal the API's AUTH_AUDIENCE). OMIT it for a temper-issued (tmpr_) credential — Temper's AS ignores a request-supplied audience entirely and mints with its server-side AS_AUDIENCE. |
STEWARD_MODEL | optional | The primary model, as an AI Gateway model id (same form as the default, minimax/minimax-m3). A change needs a redeploy, and a typo fails the build. |
STEWARD_MODEL_FALLBACKS | optional | Comma-separated AI Gateway model ids, tried in order after the primary fails. Defaults to anthropic/claude-haiku-4.5. Deduped, and the primary is dropped from the list if repeated there. |
TEMPER_CONNECT_CONNECTOR | fallback | Vercel Connect connector id. Used only when TEMPER_M2M_CLIENT_ID is unset. On an Auth0-fronted instance this cannot mint an app token — see below. Steward only — a connector is deployment-scoped, so it can never identify the auditor. |
TEMPER_TOKEN | dev only | An already-OAuth-obtained temper token. Drives eve dev. Cannot re-mint, so a 401 on it is terminal. |
TEMPER_AUDITOR_M2M_CLIENT_ID | auditor, prod | The auditor's own machine client id — a second Auth0 M2M app, or a second tmpr_…. Never the steward's. |
TEMPER_AUDITOR_M2M_CLIENT_SECRET | auditor, prod | The auditor's client secret. Vercel env only. |
TEMPER_AUDITOR_M2M_TOKEN_URL | auditor, prod | Same issuer as the steward's — one instance, one issuer. Only the credential differs. |
TEMPER_AUDITOR_M2M_AUDIENCE | external IdP only | Same rule as TEMPER_M2M_AUDIENCE: required for Auth0, omitted for a tmpr_ credential. |
TEMPER_AUDITOR_TOKEN | dev only | Static auditor bearer for eve dev. The unset-with-no-CLIENT_ID case throws rather than silently falling back to the steward's identity. |
TEMPER_AUDITOR_ENABLED | optional | Opt-OUT. Turns agent maintenance off on a deployment that holds auditor credentials and still wants the cron idle. Absence means ENABLED, as does a declared-but-empty value. Only 0/false/off/no turn it off; 1/true/on/yes turn it on explicitly and say nothing. Any OTHER value runs the auditor and logs a warning naming the variable — a typo must not be read as an instruction to stop. Deliberately the inverse polarity of every other variable here: a merge may not turn a production cron off any more than on. |
AUDITOR_MODEL | optional | The auditor's primary model. Defaults to anthropic/claude-haiku-4.5 — deliberately not the steward's default. Same build-time freeze and redeploy-to-change semantics. |
AUDITOR_MODEL_FALLBACKS | optional | Defaults to minimax/minimax-m3 (the steward's primary — a documented availability trade). Set to "" to make the tick fail rather than collapse the two personas onto one model. |
temper-auth.ts, and is machine-identity-first:TEMPER_M2M_CLIENT_ID present → mint via the OAuth client_credentials grantClientCredentials from temper-ts). The production path.TEMPER_CONNECT_CONNECTOR → a Vercel Connect app token.TEMPER_TOKEN → a static bearer.mintM2mToken to eve asauth.getToken) and the code schedules (via temperFetch), so the two can never drift on howtemperFetchre-mints once on a 401 and retries. Refresh-ahead-of-expiry is not
sufficient: a schedule resolves a token, then fans out N fetches, and Temper's AS mints
900-second tokens by default — a tick outliving its token is ordinary, not exotic.
Exactly one re-mint: a 401 that survives a fresh token is a real authorization failure
(revoked credential, missing reach), and retrying forever would only bury it. A strategy
that cannot mint (TEMPER_TOKEN) gets its 401 back untouched.temperFetchalso carries
the 5xx cold-start retry — use it, never a barefetch.
agent.ts at build time and freezes the resolved model into the compiledmodel-config.ts):STEWARD_MODEL fails the build, not a 3am cron tick.STEWARD_MODEL and redeploying.minimax/minimax-m3, falling back to anthropic/claude-haiku-4.5) is a costVERCEL_OIDC_TOKEN is injected at runtime) —eve dev; aftervercel link, vercel env pull writes it into .env.local.TEMPER_CONNECT_CONNECTOR is still a live strategy in the code, used whenTEMPER_M2M_CLIENT_ID is unset. temper-mcp is a full OAuth 2.0 server and serves theTEMPER_MCP_URL.scl_…) and a UID of the form<host>/<name>. Either form is a valid TEMPER_CONNECT_CONNECTOR value.On an Auth0-fronted instance the Connect apppath cannot mint a token, and this is not
fixable from Temper's side. Auth0 issuesclient_credentialsonly for a registered M2M
application, and the Connect connector has no Auth0 M2M app behind it — its dynamic
registration does not create one. TheTEMPER_M2M_*vars are the real path. Connect
remains in the code for instances where it does work.
defineSchedule becomes a Vercel Cron Job,0 * * * *, and the auditor dispatch tick daily at 30 3 * * * — on| log line says | what it means |
|---|---|
no auditor credential on this deployment | You do not run an auditor. The intended resting state for most deployments. |
TEMPER_AUDITOR_ENABLED turns agent maintenance off | You do run one, and have deliberately paused it. Unset the variable to resume. |
the token endpoint will not mint … right now (issuance quota) | Your credentials are correct and your IdP is refusing to issue more M2M tokens this period. No work was claimed. It resumes on its own when the quota does. |
[steward-dispatch] tick <correlation-id> starting, then the claimed-job count (or(no drift)), then fans out. [steward-materialize] logs its candidate count. An401 on /dispatch, not askb_invocations: status / outcome / closed_at / correlation_id) and its actskb_events joined on invocation_id). Read them with the MCP tools invocation_show <id>invocation_list --status open (any orphanedinvocation_show returns only the acts the caller can read: an actAlways snapshot prod before a hand-run DDL/data change. Create a copy-on-write Neon
backup branch first (neonctl branches create … --parent main); restore withneonctl branches restore main <backup-name>.
open invocation with no outcome and (depending onopen past the function's max execution duration AND noinvocation_show (is closed_at set? are acts stillinvocation_list --status open — don't conclude from a single mid-runopen well after the function could have run) meansinvocation_close. It is harmless cruft (append-only), but it is a signal worthsteward_ingest_delta: cognitive map not found is an access-scoped not-found, not an401. It means the credential authenticated but its profile has no reach to that map — go--team / --cogmap reach you registered it with. A genuine auth401 with an explicit message naming thevw_agent_exercise answers it in one query, one row per registered machine principal — not perkb_machine_clients rows (reactivation is a newWHERE clause on revocation. A principal whose credentials have all beencredentials_live = 0| What is stale | Reading |
|---|---|
last_seen_at was never set at all | the credential has never authenticated — check registration and admission |
every rung together, last_seen_at included | the credential is not reaching this instance. Either it has been revoked (credentials_live = 0) or its issuer is refusing to mint for it, and the agent skipped quietly — see below |
last_session_opened_at, while last_seen_at is fresh | authenticating and not running. Benign if the queue was empty; a fault if it was not |
last_session_closed_at, with last_session_status = open | a session started and never finished — a function timeout, or a tick that died mid-loop |
last_emitted_at, while the session rungs are fresh | ran clean and changed nothing in the corpus |
last_seen_at. An agent refused at the token endpoint never reaches this instance atlast_seen_at sitting above an old session — which means the agent is authenticating and is notlast_session_closed_at later than last_session_opened_at is the healthy just-finished case;last_session_opened_at later is the healthy mid-run case. "Where the signal stops" is aboutlast_seen_at moving withlast_session_opened_at far behind is the steady state of a healthy agent with nothing to do — theclaimed 0 job(s) below. Distinguish them by whether the queue actually held work,kb_citation_audits, one row per (block, source) weighed, eachaudited_by_profile_id (filled by the projector from the owning event'sGET /api/resources/{id}/citation-audits reads the attributed trail for any finding you can[auditor-dispatch] tick <corr>: claimed N job(s): <job>→<cogmap>(M citation(s) across K finding(s))claimed 0 job(s) (no auditable citations) is the steady state, not a fault. Oncecitations list on a claimed job is skipped, not failed. The schedule skips404 on an audit write is the self-audit denial arm, and it is almost always a reachAuditAuthority is readability minus a self-audit denial, and bothNotFound (so the write can never become an existence oracle). IfAuthor for every finding there — or is sharing the steward's client id. Check the--cogmap suffix before suspecting the data.This is why the auditor must be its own principal, restated from the write side. The
filter is per principal, not per citation: a citation another principal weighed is still
offered, because cross-principal audit is the entire premise. Point two agents at one
client id and they become one principal — each silently suppressing the other's remaining
work.
machine-token-contract.md §C —temperFetch: temper-auth.ts.model-config.ts.steward.ts, materialize.ts, auditor.ts.ClientCredentials mint, correct against both issuers: credentials.ts —instructions.md.