Skip to content

Implement lease-bound provisioned credentials (spec §9.7, §9.8) #55

@nficano

Description

@nficano

Goal

Implement lease-bound provisioned credentials, as specified in ARCP v1.1 §9.8, and the supporting model.use lease capability (§9.7).

This is a runtime-side feature: when a job is accepted, the runtime mints one or more short-lived, scope-restricted credentials at an upstream cost-bearing service (LLM gateway, search API, paid SaaS), embeds them in job.accepted.payload.credentials, and revokes them on job termination. The upstream becomes the cost / model-tier enforcement backstop instead of the agent self-policing.

The wire shape is vendor-neutral. LiteLLM's /key/generate is the canonical reference backend (one-shot virtual key with max_budget and allowed_models matched to the lease, revoked via /key/delete), but the SDK must not bake that vendor in.

Scope

Lease grammar

  • Parse model.use capability patterns from lease_request.
  • Enforce model.use on any LLM invocation the runtime is in the path of (PERMISSION_DENIED on miss).
  • Extend lease subsetting (§9.4) to cover model.use: a child's permitted model set must be a subset of the parent's. Reject with LEASE_SUBSET_VIOLATION otherwise.
  • When cost.budget is enforced through a provisioned credential, translate upstream budget-exhausted errors into BUDGET_EXHAUSTED at the ARCP boundary (§9.6).

Provisioned credentials

  • Define a CredentialProvisioner interface (or this language's idiomatic equivalent) with issue(lease, jobContext) -> Credential[] and revoke(credentialId). Vendor-specific implementations (e.g., LiteLLM, Anthropic admin keys, custom) live as plug-ins, not in core.
  • Wire the provisioner into job acceptance: call issue after the lease is finalized, attach the returned credentials array to job.accepted.payload, before the message is sent.
  • Each credential matches the wire shape in §9.8.1: {id, scheme, value, endpoint, profile?, constraints?}. scheme: "bearer" is the minimum; other schemes are optional.
  • Bake into each credential, at minimum: cost.budget → upstream spend cap; model.use → upstream allowed-model list; lease_constraints.expires_at → credential TTL.
  • On terminal state (success, error, cancelled, timed_out), call revoke for every outstanding credential. Best-effort with retry on transient failure; persist outstanding credential IDs so revocation survives runtime restarts.
  • Support credential rotation: when the provisioner re-issues mid-job, emit a status event with phase: "credential_rotated" carrying {id, value}. Revoke the prior value promptly.
  • Delegated jobs (§10) receive child credentials constrained at or below the child's lease. Child credentials revoke with the child, not the parent.

Feature negotiation

  • Advertise provisioned_credentials and model.use in session.welcome.payload.capabilities.features only when a provisioner is configured.
  • Accept both flags from session.hello.payload.capabilities.features and respect the intersection rule (§6.2).

Security

  • Treat credential value as a secret throughout: no logs, no telemetry export, no echo to subscribers.
  • Redact credentials from any session.list_jobs / introspection surface presented to a principal that is not the job's submitter (§14 "Credential confidentiality").
  • Reject configurations that advertise provisioned_credentials without a durable revocation path (§14 "Credential revocation reliability").
  • Issue credentials only over authenticated, encrypted transports.

Tests

  • Unit: lease parser accepts model.use patterns; subsetting rejects expanded model sets.
  • Unit: BUDGET_EXHAUSTED translation from a stubbed upstream error.
  • Integration: in-memory CredentialProvisioner that returns deterministic credentials; verify they appear in job.accepted, are absent from cross-principal introspection, and revoke is called on every terminal state including cancelled and timed_out.
  • Integration: credential rotation emits status: credential_rotated and revokes the prior value.
  • Integration: delegated job receives a child credential whose constraints are a strict subset.

Docs / examples

  • Update docs/guides/leases.md (or equivalent) with model.use semantics and the credential lifecycle.
  • Add a recipe / example demonstrating a LiteLLM-backed provisioner. Keep it in examples/ or recipes/ so it's clearly a plug-in, not core.
  • Update CONFORMANCE.md to claim model.use and provisioned_credentials once the work lands.

Non-goals

  • Defining a credential-scheme registry beyond bearer. Other schemes (basic, signed_url, etc.) are deferred until a concrete use case appears.
  • Predictive cost accounting. The upstream is authoritative; the runtime translates errors, it does not estimate.
  • Built-in adapters for specific vendors beyond a documented reference plug-in. Core ships the interface only.

References

Implementation prompt

The repo is a pnpm workspace. Package layout: packages/core, packages/client, packages/runtime, packages/sdk, plus middleware adapters under packages/middleware/*. Tests use vitest. Schemas use effect/Schema. All paths below are absolute from the repo root.

Files to touch

@arcp/core — wire shapes, error codes, feature flags, capability namespace registry.

  • packages/core/src/messages/lease-schema.ts — add "model.use" to RESERVED_CAPABILITY_NAMES (currently lists six names; this becomes seven). Update the JSDoc on RESERVED_CAPABILITY_NAMES and the ReservedCapabilityName type comment.
  • packages/core/src/messages/credentials.ts (new) — define CredentialSchema, Credential type, CredentialConstraintsSchema, CredentialConstraints type. Mirrors §9.8.1.
  • packages/core/src/messages/execution.ts — extend JobAcceptedPayloadSchema with credentials?: Credential[]. Extend JobSubscribedPayloadSchema likewise. Import from ./credentials.js.
  • packages/core/src/messages/index.ts — re-export from ./credentials.js (already uses export *; just make the new file is picked up via index.ts aggregation if needed — check that the new file isn't excluded).
  • packages/core/src/version.ts — append "model.use" and "provisioned_credentials" to V1_1_FEATURES.

@arcp/runtime — provisioner interface, lifecycle wiring, revocation store, redaction.

  • packages/runtime/src/credential-provisioner.ts (new) — CredentialProvisioner interface, CredentialIssueContext type, IssuedCredential type (server-side bookkeeping that wraps the wire Credential plus a provisionerId for revoke).
  • packages/runtime/src/credential-store.ts (new) — CredentialStore interface and InMemoryCredentialStore implementation (parallels IdempotencyStore / ResumeStore in stores.ts). Persists {jobId, credentialId, provisionerId, issuedAt} so revocation survives restart in pluggable backends.
  • packages/runtime/src/lease.ts — extend validateLeaseShape to accept model.use (it already accepts reserved names via isValidCapabilityName, so the change is purely additive in the core schema). Verify isLeaseSubset already handles a model.use capability as a plain glob-list subset (no special case needed since model identifiers are non-slash strings; the existing patternSubsumes works). Add explicit JSDoc noting model.use per §9.4.
  • packages/runtime/src/types.ts — add credentialProvisioner?: CredentialProvisioner and credentialStore?: CredentialStore fields to ARCPServerOptions. Add credentials?: readonly IssuedCredential[] to JobOptions.
  • packages/runtime/src/job.ts — add credentials: IssuedCredential[] field on Job. In emitAccepted, include credentials: this.credentials.map(toWireCredential) when the array is non-empty. Add applyCredentialRotation(prev, next) and revokeAll() helpers.
  • packages/runtime/src/job-runner.ts — after constructJob, call provisioner.issue(...) (when configured), assign the issued credentials onto the Job, then call job.emitAccepted(). In runHandler's finally block (after ctx.jobs.retire(...)), call job.revokeAll() regardless of terminal state. Mirror in createDelegateJob so delegated child jobs get their own credentials.
  • packages/runtime/src/server.ts — in makeNegotiatedCapabilities / acceptFreshSession, only advertise "provisioned_credentials" and "model.use" when options.credentialProvisioner !== undefined (i.e., filter advertisedFeatures here). Validate at server construction that, if credentialProvisioner is configured, credentialStore is also configured (durable revocation path requirement §14).
  • packages/runtime/src/server-subscribe.tsbuildSubscribedPayload MUST NOT include credentials for subscribers that are not the submitter; gate by defaultJobAuthorizationPolicy plus a strict "same principal" check. buildListJobsCandidates similarly excludes credentials from any non-submitter view (it already excludes them by omission — confirm by reading the function; just add the field cautiously).
  • packages/runtime/src/index.ts — re-export CredentialProvisioner, CredentialIssueContext, IssuedCredential, CredentialStore, InMemoryCredentialStore, and the new error-translation helper.

@arcp/client — surface credentials on JobHandle.

  • packages/client/src/client-handle.ts — add credentials?: readonly Credential[] to InvocationState and JobHandle. Plumb through makeHandleFromInvocation.
  • packages/client/src/types.ts — add credentials getter to JobHandle (mirroring lease, agent, budget).
  • packages/client/src/client.ts — on inbound job.accepted, copy env.payload.credentials (if present) into the invocation state.

Tests — vitest, alongside the existing files.

  • packages/runtime/test/lease.test.ts — add model.use glob, subset, and rejection tests.
  • packages/runtime/test/credential-provisioner.test.ts (new) — unit tests for in-memory provisioner lifecycle.
  • packages/sdk/test/integration/provisioned-credentials.test.ts (new) — end-to-end tests covering all integration acceptance items.

Docs — markdown.

  • docs/guides/leases.md — add a model.use section after the namespace table.
  • docs/guides/credentials.md (new) — full guide for the credential lifecycle, with the LiteLLM mapping table.
  • examples/provisioned-credentials/ (new) — server.ts, client.ts, README.md. Use an in-memory MockProvisioner to keep the example free of an external dependency.
  • recipes/litellm-credentials/ (new) — full LiteLLM-backed recipe with server.ts, client.ts, README.md. Wire to /key/generate and /key/delete over HTTP.
  • recipes/README.md — link the new recipe.
  • examples/README.md — link the new example.
  • CONFORMANCE.md — claim model.use and provisioned_credentials in the §9 and §6.2 tables.

Public API additions

// packages/core/src/messages/credentials.ts (new)
import { Schema } from "effect";

import { LeaseConstraintsSchema } from "./lease-schema.js";

/** v1.1 §9.8.1 — per-credential constraints echoed on the wire. */
export const CredentialConstraintsSchema = Schema.Struct({
  expires_at: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())),
  allowed_models: Schema.optional(
    Schema.mutable(Schema.Array(Schema.String.pipe(Schema.nonEmptyString()))),
  ),
  max_spend: Schema.optional(
    Schema.Struct({
      currency: Schema.String.pipe(Schema.nonEmptyString()),
      amount: Schema.Number.pipe(Schema.nonNegative()),
    }),
  ),
});
export type CredentialConstraints = Schema.Schema.Type<
  typeof CredentialConstraintsSchema
>;

/** v1.1 §9.8.1 — wire shape of one issued credential. */
export const CredentialSchema = Schema.Struct({
  id: Schema.String.pipe(Schema.nonEmptyString()),
  scheme: Schema.Literal("bearer"),
  value: Schema.String.pipe(Schema.nonEmptyString()),
  endpoint: Schema.String.pipe(Schema.nonEmptyString()),
  profile: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())),
  constraints: Schema.optional(CredentialConstraintsSchema),
});
export type Credential = Schema.Schema.Type<typeof CredentialSchema>;
// packages/runtime/src/credential-provisioner.ts (new)
import type { Credential, Lease, LeaseConstraints } from "@arcp/core/messages";
import type { JobId, TraceId } from "@arcp/core";

/** Server-side bookkeeping wrapper around a wire `Credential`. */
export interface IssuedCredential {
  /** Wire-form credential — exactly what is embedded in `job.accepted`. */
  readonly wire: Credential;
  /** Opaque ID this provisioner uses to revoke. */
  readonly provisionerId: string;
}

export interface CredentialIssueContext {
  readonly jobId: JobId;
  readonly parentJobId?: JobId | undefined;
  readonly lease: Lease;
  readonly leaseConstraints: LeaseConstraints | undefined;
  readonly initialBudget: ReadonlyMap<string, number>;
  readonly principal: string | undefined;
  readonly traceId: TraceId | undefined;
}

/** v1.1 §9.8 — pluggable upstream credential minter. */
export interface CredentialProvisioner {
  issue(ctx: CredentialIssueContext): Promise<readonly IssuedCredential[]>;
  revoke(provisionerId: string): Promise<void>;
}

/**
 * Translate a vendor error to an ARCP `BudgetExhaustedError`. Plug-ins call
 * this when the upstream signals a spend cap hit.
 */
export function toBudgetExhausted(error: unknown, details?: Record<string, unknown>): never;
// packages/runtime/src/credential-store.ts (new)
export interface CredentialStoreEntry {
  readonly jobId: string;
  readonly credentialId: string;
  readonly provisionerId: string;
  readonly issuedAt: number;
}

export interface CredentialStore {
  add(entry: CredentialStoreEntry): Promise<void>;
  removeByJob(jobId: string): Promise<readonly CredentialStoreEntry[]>;
  listOutstanding(): Promise<readonly CredentialStoreEntry[]>;
}

export class InMemoryCredentialStore implements CredentialStore { /* ... */ }
// packages/runtime/src/types.ts — extensions
export interface ARCPServerOptions {
  // ...existing fields...
  /** v1.1 §9.8 — pluggable credential minter. When set, the runtime advertises `provisioned_credentials`. */
  credentialProvisioner?: CredentialProvisioner;
  /** v1.1 §9.8 — durable revocation tracking. REQUIRED when `credentialProvisioner` is set. */
  credentialStore?: CredentialStore;
}

export interface JobOptions {
  // ...existing fields...
  credentials?: readonly IssuedCredential[];
}
// packages/client/src/types.ts — JobHandle extension
export interface JobHandle {
  // ...existing fields...
  /** v1.1 §9.8 — credentials minted for this job, if the runtime advertised `provisioned_credentials`. */
  readonly credentials: readonly Credential[] | undefined;
}

Step-by-step changes

  1. core: feature flags — In packages/core/src/version.ts, append "model.use" and "provisioned_credentials" to V1_1_FEATURES. Do NOT reorder existing entries (downstream tests assert sorted equality).

  2. core: capability name — In packages/core/src/messages/lease-schema.ts, add "model.use" to RESERVED_CAPABILITY_NAMES. Update JSDoc on the constant and the ReservedCapabilityName doc comment.

  3. core: credential wire schema — Create packages/core/src/messages/credentials.ts with CredentialConstraintsSchema, CredentialSchema, and the inferred types. Use the same effect/Schema style as lease-schema.ts.

  4. core: extend JobAcceptedPayloadSchema — In packages/core/src/messages/execution.ts, import CredentialSchema from ./credentials.js and add credentials: Schema.optional(Schema.mutable(Schema.Array(CredentialSchema))) to JobAcceptedPayloadSchema. Mirror on JobSubscribedPayloadSchema (so subscribers who pass authorization see it).

  5. core: re-exports — Ensure packages/core/src/messages/index.ts exports the new file (it uses export * for the directory, so creating the file may need a sibling re-export — check by running typecheck). If the index does not automatically pick up credentials.ts, add export * from "./credentials.js".

  6. runtime: provisioner interface — Create packages/runtime/src/credential-provisioner.ts. Define CredentialProvisioner, IssuedCredential, CredentialIssueContext, and a toBudgetExhausted(error, details?) helper that constructs a BudgetExhaustedError (import { BudgetExhaustedError } from "@arcp/core/errors").

  7. runtime: credential store — Create packages/runtime/src/credential-store.ts. Implement InMemoryCredentialStore keyed by jobIdMap<credentialId, entry>. Provide add, removeByJob (returns and clears the per-job list), listOutstanding.

  8. runtime: types extension — In packages/runtime/src/types.ts, add credentialProvisioner and credentialStore to ARCPServerOptions. Add credentials?: readonly IssuedCredential[] to JobOptions. Add a re-export note (no value re-export needed; types are pulled by index.ts).

  9. runtime: Job class — In packages/runtime/src/job.ts:

    • Add public credentials: IssuedCredential[] = [] field.
    • Constructor: this.credentials = [...(options.credentials ?? [])].
    • In emitAccepted, include credentials: this.credentials.map((c) => c.wire) in the payload when this.credentials.length > 0.
    • Add public async rotateCredential(prevId: string, next: IssuedCredential): Promise<void> — replaces in-place, emits a status event with phase: "credential_rotated" body { id: next.wire.id, value: next.wire.value } (DO NOT log the value).
    • Add public async revokeAll(provisioner: CredentialProvisioner | undefined, store: CredentialStore | undefined, logger: Logger): Promise<void> — iterates credentials, calls provisioner.revoke(c.provisionerId) with one-retry-on-throw, removes entries from store. Logger entries MUST omit c.wire.value.
  10. runtime: server option validation — In packages/runtime/src/server.ts ARCPServer constructor, if options.credentialProvisioner !== undefined && options.credentialStore === undefined, throw new InvalidRequestError("credentialProvisioner requires credentialStore for durable revocation (§14)").

  11. runtime: feature gating — In packages/runtime/src/server.ts, modify advertisedFeatures (or the call site in makeNegotiatedCapabilities) so that "provisioned_credentials" and "model.use" are filtered OUT when options.credentialProvisioner === undefined. Keep them in when configured.

  12. runtime: issue on submit — In packages/runtime/src/job-runner.ts acceptAndDispatchSubmit:

    • After constructJob(...) and before job.emitAccepted(), if this.server.options.credentialProvisioner is defined, call:
      const issued = await this.server.options.credentialProvisioner.issue({
        jobId: job.jobId,
        ...(job.parentJobId === undefined ? {} : { parentJobId: job.parentJobId }),
        lease: job.lease,
        leaseConstraints: job.leaseConstraints,
        initialBudget: job.initialBudget,
        principal: job.submitterPrincipal,
        traceId: job.traceId,
      });
      job.credentials.push(...issued);
      for (const c of issued) {
        await this.server.options.credentialStore!.add({ jobId: job.jobId, credentialId: c.wire.id, provisionerId: c.provisionerId, issuedAt: Date.now() });
      }
    • Wrap issue in try/catch. On throw, emit job.error { final_status: "error", code: "INTERNAL_ERROR" } and abort the submit before emitAccepted.
  13. runtime: revoke on terminal — In runHandler's finally block, after ctx.jobs.retire(job.jobId), call:

    await job.revokeAll(
      this.server.options.credentialProvisioner,
      this.server.options.credentialStore,
      job.logger,
    );
  14. runtime: delegated jobs — In createDelegateJob, after constructDelegateChild and before child.emitAccepted(), issue child credentials via the same path as #12, passing parentJobId: parent.jobId. The provisioner is expected to subset constraints; the runtime does not re-validate the value-bearing payload, only that the lease passed to issue is a strict subset (which is already enforced by validateDelegateLease).

  15. runtime: subscriber redaction — In packages/runtime/src/server-subscribe.ts buildSubscribedPayload, do NOT emit credentials when the subscriber's principal is not the job's submitter. Inline check: const isSubmitter = ctx.state.identity?.principal === job.submitterPrincipal; and only spread credentials when isSubmitter && job.credentials.length > 0. Pass ctx as a new arg into buildSubscribedPayload (already available at the caller).

  16. runtime: list-jobs scoping — In packages/runtime/src/server-subscribe.ts buildListJobsCandidates, the existing entry shape (JobListEntry) does not include credentials — confirm and leave as-is (it is already redacted by omission). Add a JSDoc comment noting §14 compliance.

  17. runtime: index re-exports — In packages/runtime/src/index.ts, add named exports for CredentialProvisioner, CredentialIssueContext, IssuedCredential, CredentialStore, InMemoryCredentialStore, and toBudgetExhausted.

  18. client: handle plumbing — In packages/client/src/client-handle.ts:

    • Add credentials: Credential[] | undefined to InvocationState (initialize undefined).
    • Add credentials getter to makeHandleFromInvocation.
      In packages/client/src/client.ts:
    • In the job.accepted route, copy env.payload.credentials (if present) onto invocation.credentials.
      In packages/client/src/types.ts:
    • Add readonly credentials: readonly Credential[] | undefined; to JobHandle, importing Credential from @arcp/core/messages.
  19. sdk: re-exports — In packages/sdk/src/runtime.ts (or index.ts), re-export the new symbols (CredentialProvisioner, IssuedCredential, CredentialIssueContext, CredentialStore, InMemoryCredentialStore, toBudgetExhausted, Credential, CredentialConstraints). Match the existing pattern (see how validateLeaseOp is re-exported).

  20. CONFORMANCE — Add rows to the §6.2 features table (provisioned_credentials, model.use) and a new §9.7/§9.8 subsection in CONFORMANCE.md, pointing at the new files.

  21. Docs guide — Update docs/guides/leases.md: add model.use to the namespace table (gates LLM model invocation). Create docs/guides/credentials.md covering issue/revoke/rotate semantics and the LiteLLM mapping table (lease cost.budgetmax_budget, model.useallowed_models, lease_constraints.expires_at → key TTL).

  22. Example — Create examples/provisioned-credentials/ with server.ts, client.ts, README.md. Use an in-memory MockProvisioner that returns deterministic { id, value: "mock-key-${i}", endpoint: "http://localhost/api" }. Mirror the layout of examples/cost-budget/.

  23. Recipe — Create recipes/litellm-credentials/ with server.ts, client.ts, README.md. Implement LiteLLMProvisioner calling POST /key/generate (body: { max_budget, allowed_models, duration }) and POST /key/delete (body: { keys: [...] }). Use fetch (Node 22+). The recipe imports CredentialProvisioner from @arcp/sdk. Add link to recipes/README.md.

Tests to add

  • packages/runtime/test/lease.test.ts — extend existing file:

    • model.use lease passes validateLeaseShape.
    • model.use glob gpt-4* matches gpt-4o-mini; gpt-3.* rejects claude-3-haiku.
    • isLeaseSubset({ "model.use": ["gpt-4*"] }, { "model.use": ["**"] }) is true.
    • isLeaseSubset({ "model.use": ["**"] }, { "model.use": ["gpt-4*"] }) is false.
  • packages/runtime/test/credential-provisioner.test.ts (new):

    • InMemoryCredentialStore.add then removeByJob returns and clears the right entries.
    • toBudgetExhausted(new Error("upstream 402")) throws BudgetExhaustedError with code "BUDGET_EXHAUSTED", retryable: false.
    • A mock provisioner that returns one credential is invoked by issue and revoke exactly once each.
  • packages/sdk/test/integration/provisioned-credentials.test.ts (new), using makePairedHarness from packages/sdk/test/helpers/fixtures.ts:

    • With a MockProvisioner configured, a submitted job receives handle.credentials of length 1 with the expected id, endpoint, and a redacted-shape value.
    • job.accepted.payload.credentials[0].constraints.allowed_models matches the lease's model.use patterns.
    • Cancel a job (handle.cancel("user")) → assert provisioner.revoke was called for that credential id.
    • Force a TIMEOUT via max_runtime_sec: 0.1 and a long handler → assert revoke fires.
    • A delegated job uses a subset model list; assert the child's credential's allowed_models is a strict subset of the parent's.
    • Server WITHOUT a provisioner: assert client.negotiatedFeatures does not include "provisioned_credentials" or "model.use".
    • Rotation: provisioner re-issues mid-job → a status event with phase: "credential_rotated" is observed on the client; the previous provisionerId was passed to revoke.
    • Cross-principal subscribe: when a second principal subscribes (via a custom jobAuthorizationPolicy that allows it), the job.subscribed.payload MUST NOT carry credentials.

Verification commands

Run from /Users/nficano/code/arpc/typescript-sdk:

pnpm install
pnpm -F @arcp/core typecheck && pnpm -F @arcp/core test
pnpm -F @arcp/runtime typecheck && pnpm -F @arcp/runtime test
pnpm -F @arcp/client typecheck && pnpm -F @arcp/client test
pnpm -F @arcp/sdk typecheck && pnpm -F @arcp/sdk test
pnpm typecheck
pnpm test
pnpm lint
pnpm check:cycles

Target a single test file while iterating:

pnpm -F @arcp/sdk exec vitest run test/integration/provisioned-credentials.test.ts
pnpm -F @arcp/runtime exec vitest run test/credential-provisioner.test.ts

Acceptance

Lease grammar:

  • [task] model.use added to RESERVED_CAPABILITY_NAMES in packages/core/src/messages/lease-schema.ts.
  • [task] validateLeaseShape accepts model.use patterns; validateLeaseOp enforces them with PERMISSION_DENIED on miss.
  • [task] isLeaseSubset rejects child model.use patterns broader than the parent with LEASE_SUBSET_VIOLATION.
  • [task] toBudgetExhausted(error) translates an upstream budget error to BudgetExhaustedError (packages/runtime/src/credential-provisioner.ts).

Provisioned credentials:

  • [task] CredentialProvisioner interface lives in packages/runtime/src/credential-provisioner.ts with issue and revoke.
  • [task] JobRunner.acceptAndDispatchSubmit calls issue after lease finalization, before job.emitAccepted.
  • [task] Wire shape in packages/core/src/messages/credentials.ts matches §9.8.1 (id, scheme: "bearer", value, endpoint, profile?, constraints?).
  • [task] Credentials carry cost.budgetmax_spend, model.useallowed_models, lease_constraints.expires_atexpires_at.
  • [task] runHandler finally-block calls job.revokeAll for all terminal states; InMemoryCredentialStore clears the per-job entries.
  • [task] Rotation API on Job (rotateCredential) emits status: credential_rotated and revokes the prior provisionerId.
  • [task] createDelegateJob issues child credentials with a subset lease; child credentials revoke with the child.

Feature negotiation:

  • [task] provisioned_credentials and model.use are added to V1_1_FEATURES and filtered out of advertisedFeatures when no provisioner is configured.
  • [task] Negotiation intersection (intersectFeatures) excludes the feature when either peer omits it.

Security:

  • [task] revokeAll logger calls do not include wire.value (verify by code review and a regex assertion in the credential-provisioner test against the captured log output).
  • [task] buildSubscribedPayload and buildListJobsCandidates redact credentials from non-submitter views.
  • [task] ARCPServer constructor throws when credentialProvisioner is set without credentialStore.

Tests:

  • [task] Lease unit tests cover model.use parse + subset.
  • [task] BUDGET_EXHAUSTED translation unit test.
  • [task] Integration tests cover: issue on submit, redact on subscribe, revoke on every terminal state, rotation, delegation subset, feature opt-out.

Docs / examples:

  • [task] docs/guides/leases.md describes model.use.
  • [task] docs/guides/credentials.md describes the lifecycle.
  • [task] examples/provisioned-credentials/ with vendor-neutral MockProvisioner.
  • [task] recipes/litellm-credentials/ wires a real LiteLLM backend.
  • [task] CONFORMANCE.md claims model.use and provisioned_credentials.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature implementationv1.1ARCP v1.1 feature work

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions