@cat-factory/server 0.274.0 → 0.275.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/agents/providerCapabilities.d.ts +14 -1
  2. package/dist/agents/providerCapabilities.d.ts.map +1 -1
  3. package/dist/agents/providerCapabilities.js +10 -1
  4. package/dist/agents/providerCapabilities.js.map +1 -1
  5. package/dist/http/runAdmission.d.ts +27 -1
  6. package/dist/http/runAdmission.d.ts.map +1 -1
  7. package/dist/http/runAdmission.js +32 -0
  8. package/dist/http/runAdmission.js.map +1 -1
  9. package/dist/modules/models/workspaceCatalog.d.ts.map +1 -1
  10. package/dist/modules/models/workspaceCatalog.js +4 -0
  11. package/dist/modules/models/workspaceCatalog.js.map +1 -1
  12. package/dist/modules/providers/personalCredentialGate.d.ts +31 -3
  13. package/dist/modules/providers/personalCredentialGate.d.ts.map +1 -1
  14. package/dist/modules/providers/personalCredentialGate.js +46 -4
  15. package/dist/modules/providers/personalCredentialGate.js.map +1 -1
  16. package/dist/modules/publicApi/PublicApiController.d.ts.map +1 -1
  17. package/dist/modules/publicApi/PublicApiController.js +85 -43
  18. package/dist/modules/publicApi/PublicApiController.js.map +1 -1
  19. package/dist/modules/publicApi/PublicApiKeyController.d.ts.map +1 -1
  20. package/dist/modules/publicApi/PublicApiKeyController.js +18 -2
  21. package/dist/modules/publicApi/PublicApiKeyController.js.map +1 -1
  22. package/dist/modules/publicApi/PublicKeyController.d.ts.map +1 -1
  23. package/dist/modules/publicApi/PublicKeyController.js +6 -0
  24. package/dist/modules/publicApi/PublicKeyController.js.map +1 -1
  25. package/dist/modules/publicApi/PublicProvisioningController.d.ts.map +1 -1
  26. package/dist/modules/publicApi/PublicProvisioningController.js +33 -11
  27. package/dist/modules/publicApi/PublicProvisioningController.js.map +1 -1
  28. package/dist/modules/publicApi/decisions/scope.d.ts +10 -1
  29. package/dist/modules/publicApi/decisions/scope.d.ts.map +1 -1
  30. package/dist/modules/publicApi/decisions/scope.js +59 -2
  31. package/dist/modules/publicApi/decisions/scope.js.map +1 -1
  32. package/dist/modules/publicApi/keyProjection.d.ts.map +1 -1
  33. package/dist/modules/publicApi/keyProjection.js +1 -0
  34. package/dist/modules/publicApi/keyProjection.js.map +1 -1
  35. package/dist/modules/publicApi/openapiDocument.generated.d.ts +1 -1
  36. package/dist/modules/publicApi/openapiDocument.generated.d.ts.map +1 -1
  37. package/dist/modules/publicApi/openapiDocument.generated.js +1 -1
  38. package/dist/modules/publicApi/openapiDocument.generated.js.map +1 -1
  39. package/dist/modules/publicApi/personalUnlock.d.ts +51 -0
  40. package/dist/modules/publicApi/personalUnlock.d.ts.map +1 -0
  41. package/dist/modules/publicApi/personalUnlock.js +45 -0
  42. package/dist/modules/publicApi/personalUnlock.js.map +1 -0
  43. package/package.json +8 -8
@@ -1,5 +1,5 @@
1
1
  import type { AccountSettingsService, ApiKeyService, LocalModelEndpointService, OpenRouterCatalogService, PersonalSubscriptionService, ProviderSubscriptionService } from '@cat-factory/integrations';
2
- import { type AppCaches, type ModelFlavor, type ProviderCapabilities } from '@cat-factory/kernel';
2
+ import { type AppCaches, type HarnessKind, type ModelFlavor, type ProviderCapabilities } from '@cat-factory/kernel';
3
3
  export interface CapabilityServices {
4
4
  apiKeys?: ApiKeyService;
5
5
  subscriptions?: ProviderSubscriptionService;
@@ -24,6 +24,19 @@ export interface CapabilityServices {
24
24
  * resolves, so the catalog + start guard don't offer a model that fails at dispatch.
25
25
  */
26
26
  baseUrlFor?: (provider: string) => string | null | undefined;
27
+ /**
28
+ * NATIVE LOCAL EXECUTION's allow-list of subscription harnesses (`AppConfig.nativeAmbientAuth`,
29
+ * from `LOCAL_NATIVE_AGENTS`; absent on every other facade).
30
+ *
31
+ * A vendor this serves runs on the developer's OWN installed CLI and its ambient login, so it
32
+ * has no credential to find: nothing is pooled, nothing is personal, and nothing is leased at
33
+ * dispatch. Without it here the deployment answered its own question two different ways — the
34
+ * personal-credential gate correctly dropped such a vendor from the set it demands an unlock
35
+ * for, while the catalog and the pipeline-start guard, reading only the two credential stores,
36
+ * still called the model unconfigured. The visible failure was a Claude-pinned run refused with
37
+ * "no configured provider" on a machine whose `claude` CLI was logged in and would have run it.
38
+ */
39
+ nativeAmbientAuth?: readonly HarnessKind[];
27
40
  /** Per-user locally-run model endpoints (resolved by the requesting/initiating user). */
28
41
  localModelEndpoints?: LocalModelEndpointService;
29
42
  /** Per-workspace enabled OpenRouter models (the dynamic catalog subset). */
@@ -1 +1 @@
1
- {"version":3,"file":"providerCapabilities.d.ts","sourceRoot":"","sources":["../../src/agents/providerCapabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,aAAa,EACb,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,EAC3B,2BAA2B,EAC5B,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAEL,KAAK,SAAS,EAEd,KAAK,WAAW,EAChB,KAAK,oBAAoB,EAE1B,MAAM,qBAAqB,CAAA;AAM5B,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,aAAa,CAAC,EAAE,2BAA2B,CAAA;IAC3C,qBAAqB,CAAC,EAAE,2BAA2B,CAAA;IACnD,sFAAsF;IACtF,uBAAuB,CAAC,EAAE,OAAO,CAAA;IACjC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC3B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAC5D,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,yBAAyB,CAAA;IAC/C,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;IAC5C;;;OAGG;IACH,eAAe,CAAC,EAAE,sBAAsB,CAAA;IACxC,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IAChF;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;;OAKG;IACH,+BAA+B,CAAC,EAAE,CAChC,WAAW,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,MAAM,KACnB,OAAO,CAAC,SAAS,WAAW,EAAE,GAAG,SAAS,CAAC,CAAA;CACjD;AA6CD,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,kBAAkB,EAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,EACtB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,oBAAoB,CAAC,CA6D/B"}
1
+ {"version":3,"file":"providerCapabilities.d.ts","sourceRoot":"","sources":["../../src/agents/providerCapabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,aAAa,EACb,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,EAC3B,2BAA2B,EAC5B,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,WAAW,EAEhB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EAE1B,MAAM,qBAAqB,CAAA;AAM5B,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,aAAa,CAAC,EAAE,2BAA2B,CAAA;IAC3C,qBAAqB,CAAC,EAAE,2BAA2B,CAAA;IACnD,sFAAsF;IACtF,uBAAuB,CAAC,EAAE,OAAO,CAAA;IACjC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC3B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAC5D;;;;;;;;;;;OAWG;IACH,iBAAiB,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;IAC1C,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,yBAAyB,CAAA;IAC/C,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;IAC5C;;;OAGG;IACH,eAAe,CAAC,EAAE,sBAAsB,CAAA;IACxC,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IAChF;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;;OAKG;IACH,+BAA+B,CAAC,EAAE,CAChC,WAAW,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,MAAM,KACnB,OAAO,CAAC,SAAS,WAAW,EAAE,GAAG,SAAS,CAAC,CAAA;CACjD;AA6CD,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,kBAAkB,EAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,EACtB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,oBAAoB,CAAC,CAsE/B"}
@@ -1,4 +1,4 @@
1
- import { ALL_SUBSCRIPTION_VENDORS, } from '@cat-factory/kernel';
1
+ import { ALL_SUBSCRIPTION_VENDORS, isAmbientNativeVendor, } from '@cat-factory/kernel';
2
2
  // Direct providers whose AI-SDK resolver works without an explicit base URL (the SDK
3
3
  // has a built-in default). Every OTHER direct provider is OpenAI-compatible and needs a
4
4
  // base URL (see `buildDirectResolver`), so it is unusable until that URL resolves.
@@ -50,6 +50,15 @@ export async function resolveWorkspaceCapabilities(services, workspaceId, userId
50
50
  baseUrlFor ? configured.filter((p) => BASE_URL_OPTIONAL.has(p) || !!baseUrlFor(p)) : configured);
51
51
  const subscriptionVendors = new Set();
52
52
  for (const vendor of ALL_SUBSCRIPTION_VENDORS) {
53
+ // Ambient FIRST, and it short-circuits both credential reads rather than joining them: a
54
+ // vendor the host CLI serves is usable for every initiator of this deployment, including one
55
+ // there is no user for at all, so neither store has anything to say about it. Decided by the
56
+ // SAME predicate the personal-credential gate and the container executor use, so the three
57
+ // halves of the ambient decision cannot drift.
58
+ if (isAmbientNativeVendor(services.nativeAmbientAuth, vendor)) {
59
+ subscriptionVendors.add(vendor);
60
+ continue;
61
+ }
53
62
  const pooled = services.subscriptions
54
63
  ? await services.subscriptions.hasToken(workspaceId, vendor)
55
64
  : false;
@@ -1 +1 @@
1
- {"version":3,"file":"providerCapabilities.js","sourceRoot":"","sources":["../../src/agents/providerCapabilities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"providerCapabilities.js","sourceRoot":"","sources":["../../src/agents/providerCapabilities.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import type { Context } from 'hono';
2
2
  import type { WorkspaceRole } from '@cat-factory/contracts';
3
3
  import type { GateActor } from '@cat-factory/kernel';
4
- import type { AppEnv } from './env.js';
4
+ import type { AppEnv, ServerContainer } from './env.js';
5
5
  /**
6
6
  * The admission facts a run start CONSUMES off the request, in one place.
7
7
  *
@@ -25,6 +25,32 @@ import type { AppEnv } from './env.js';
25
25
  * absent is deliberately not treated as the lowest tier.
26
26
  */
27
27
  export declare function runInitiatorRole<E extends AppEnv>(c: Context<E>): WorkspaceRole | null;
28
+ /**
29
+ * {@link runInitiatorRole}'s twin for a KEY-authenticated start: the role held by the user a
30
+ * public-API key is BOUND to (`PublicApiKeyRecord.actsAsUserId`), or `null` when it is bound to
31
+ * nobody.
32
+ *
33
+ * A separate function because the two read different places for the same fact. An SPA request
34
+ * arrives behind `mountAuthGate`, which has already resolved the caller's access and published it;
35
+ * `/api/v1` has no such gate, because a key is admitted by SCOPE rather than by membership, so the
36
+ * one identity a key can name has to be resolved here. It goes through the same single authority
37
+ * either way (`loadWorkspaceAccess`, ADR 0025) and so reads THROUGH its cache rather than
38
+ * re-deriving membership.
39
+ *
40
+ * Resolving it is what keeps a bound key's run under the SAME policy that person gets in the app.
41
+ * A start that pins an initiator but no role is not a run with a lenient policy, it is a run with
42
+ * no policy: `dryRunForcedForRole` sees no role to sandbox, and both role-scoped merge narrowings
43
+ * pass through unnarrowed, so the key would land what its own holder could not land from the
44
+ * board. That gap has shipped once already, on the bug-hunt adopt route, which is why the accessor
45
+ * exists at all.
46
+ *
47
+ * `null` covers two states on purpose: an unbound key (there is no person, as before) and a bound
48
+ * user whose access no longer resolves (they were removed from the roster since minting). Both are
49
+ * honestly "no tier to scope this run by" rather than a lowest tier, exactly as
50
+ * {@link runInitiatorRole} treats dev-open. The key itself stays authorized by its scope; only the
51
+ * role-shaped narrowing has nothing to say.
52
+ */
53
+ export declare function keyInitiatorRole(container: ServerContainer, workspaceId: string, userId: string | null): Promise<WorkspaceRole | null>;
28
54
  /**
29
55
  * The identity RESOLVING a parked gate, for the approver policy a pipeline step may configure
30
56
  * (`stepOptions.gateConfig.approvers`) and the quorum it may demand.
@@ -1 +1 @@
1
- {"version":3,"file":"runAdmission.d.ts","sourceRoot":"","sources":["../../src/http/runAdmission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEtC;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,aAAa,GAAG,IAAI,CAEtF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,CAWpE"}
1
+ {"version":3,"file":"runAdmission.d.ts","sourceRoot":"","sources":["../../src/http/runAdmission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAGvD;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,aAAa,GAAG,IAAI,CAEtF;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAI/B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,CAWpE"}
@@ -1,4 +1,5 @@
1
1
  import { UNATTRIBUTED_GATE_ACTOR } from '@cat-factory/kernel';
2
+ import { loadWorkspaceAccess } from './workspaceAccess.js';
2
3
  /**
3
4
  * The admission facts a run start CONSUMES off the request, in one place.
4
5
  *
@@ -24,6 +25,37 @@ import { UNATTRIBUTED_GATE_ACTOR } from '@cat-factory/kernel';
24
25
  export function runInitiatorRole(c) {
25
26
  return c.get('workspaceAccess')?.role ?? null;
26
27
  }
28
+ /**
29
+ * {@link runInitiatorRole}'s twin for a KEY-authenticated start: the role held by the user a
30
+ * public-API key is BOUND to (`PublicApiKeyRecord.actsAsUserId`), or `null` when it is bound to
31
+ * nobody.
32
+ *
33
+ * A separate function because the two read different places for the same fact. An SPA request
34
+ * arrives behind `mountAuthGate`, which has already resolved the caller's access and published it;
35
+ * `/api/v1` has no such gate, because a key is admitted by SCOPE rather than by membership, so the
36
+ * one identity a key can name has to be resolved here. It goes through the same single authority
37
+ * either way (`loadWorkspaceAccess`, ADR 0025) and so reads THROUGH its cache rather than
38
+ * re-deriving membership.
39
+ *
40
+ * Resolving it is what keeps a bound key's run under the SAME policy that person gets in the app.
41
+ * A start that pins an initiator but no role is not a run with a lenient policy, it is a run with
42
+ * no policy: `dryRunForcedForRole` sees no role to sandbox, and both role-scoped merge narrowings
43
+ * pass through unnarrowed, so the key would land what its own holder could not land from the
44
+ * board. That gap has shipped once already, on the bug-hunt adopt route, which is why the accessor
45
+ * exists at all.
46
+ *
47
+ * `null` covers two states on purpose: an unbound key (there is no person, as before) and a bound
48
+ * user whose access no longer resolves (they were removed from the roster since minting). Both are
49
+ * honestly "no tier to scope this run by" rather than a lowest tier, exactly as
50
+ * {@link runInitiatorRole} treats dev-open. The key itself stays authorized by its scope; only the
51
+ * role-shaped narrowing has nothing to say.
52
+ */
53
+ export async function keyInitiatorRole(container, workspaceId, userId) {
54
+ if (!userId)
55
+ return null;
56
+ const access = await loadWorkspaceAccess(container, workspaceId, userId);
57
+ return access?.allowed ? access.role : null;
58
+ }
27
59
  /**
28
60
  * The identity RESOLVING a parked gate, for the approver policy a pipeline step may configure
29
61
  * (`stepOptions.gateConfig.approvers`) and the quorum it may demand.
@@ -1 +1 @@
1
- {"version":3,"file":"runAdmission.js","sourceRoot":"","sources":["../../src/http/runAdmission.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAG7D;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAmB,CAAa;IAC9D,OAAO,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,IAAI,IAAI,IAAI,CAAA;AAC/C,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAmB,CAAa;IACvD,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,EAAE,EAAE,uBAAuB,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAA;IACzF,CAAC;IACD,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC;QACzB,KAAK,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK;KAC/B,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"runAdmission.js","sourceRoot":"","sources":["../../src/http/runAdmission.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAE7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAE1D;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAmB,CAAa;IAC9D,OAAO,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,IAAI,IAAI,IAAI,CAAA;AAC/C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,SAA0B,EAC1B,WAAmB,EACnB,MAAqB;IAErB,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACxB,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;IACxE,OAAO,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AAC7C,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAmB,CAAa;IACvD,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,EAAE,EAAE,uBAAuB,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAA;IACzF,CAAC;IACD,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC;QACzB,KAAK,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK;KAC/B,CAAA;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceCatalog.d.ts","sourceRoot":"","sources":["../../../src/modules/models/workspaceCatalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAO1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAGxD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,4BAA4B,CAChD,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,YAAY,CAAC,CAqCvB"}
1
+ {"version":3,"file":"workspaceCatalog.d.ts","sourceRoot":"","sources":["../../../src/modules/models/workspaceCatalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAO1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAGxD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,4BAA4B,CAChD,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,YAAY,CAAC,CAyCvB"}
@@ -32,6 +32,10 @@ export async function resolveWorkspaceModelCatalog(container, workspaceId, userI
32
32
  // dispatch, where the block is in hand.
33
33
  const caps = await resolveWorkspaceCapabilities({
34
34
  ...container,
35
+ // Not supplied by the container spread: it lives on `config`, not beside the services.
36
+ ...(container.config.nativeAmbientAuth
37
+ ? { nativeAmbientAuth: container.config.nativeAmbientAuth }
38
+ : {}),
35
39
  accountSettings: container.accountSettings?.service,
36
40
  workspaceAccountOf: (ws) => container.workspaceService.accountOf(ws),
37
41
  modelPolicySupported: container.config.infrastructure?.modelPolicy?.supported ?? false,
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceCatalog.js","sourceRoot":"","sources":["../../../src/modules/models/workspaceCatalog.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEzE,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAA;AAEnF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,SAA0B,EAC1B,WAAmB,EACnB,MAAe;IAEf,MAAM,OAAO,GAAG,SAAS,CAAC,YAAY,EAAE,OAAO,CAAA;IAC/C,gGAAgG;IAChG,uFAAuF;IACvF,gGAAgG;IAChG,oEAAoE;IACpE,EAAE;IACF,4FAA4F;IAC5F,+FAA+F;IAC/F,gGAAgG;IAChG,wCAAwC;IACxC,MAAM,IAAI,GAAG,MAAM,4BAA4B,CAC7C;QACE,GAAG,SAAS;QACZ,eAAe,EAAE,SAAS,CAAC,eAAe,EAAE,OAAO;QACnD,kBAAkB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,gBAAgB,CAAC,SAAS,CAAC,EAAE,CAAC;QACpE,oBAAoB,EAAE,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,WAAW,EAAE,SAAS,IAAI,KAAK;QACtF,GAAG,CAAC,OAAO;YACT,CAAC,CAAC,EAAE,+BAA+B,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,OAAO,CAAC,qBAAqB,CAAC,EAAE,CAAC,EAAE;YACxF,CAAC,CAAC,EAAE,CAAC;KACR,EACD,WAAW,EACX,MAAM,CACP,CAAA;IACD,MAAM,KAAK,GACT,MAAM,IAAI,SAAS,CAAC,mBAAmB;QACrC,CAAC,CAAC,MAAM,SAAS,CAAC,mBAAmB,CAAC,eAAe,CAAC,MAAM,CAAC;QAC7D,CAAC,CAAC,EAAE,CAAA;IACR,MAAM,UAAU,GAAG,SAAS,CAAC,iBAAiB;QAC5C,CAAC,CAAC,MAAM,SAAS,CAAC,iBAAiB,CAAC,eAAe,CAAC,WAAW,CAAC;QAChE,CAAC,CAAC,EAAE,CAAA;IACN,MAAM,OAAO,GAAG,iBAAiB,CAAC,iBAAiB,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAA;IACxF,OAAO,oBAAoB,CACzB,CAAC,GAAG,qBAAqB,CAAC,KAAK,CAAC,EAAE,GAAG,0BAA0B,CAAC,UAAU,CAAC,CAAC,EAC5E,IAAI,EACJ,OAAO,CACR,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"workspaceCatalog.js","sourceRoot":"","sources":["../../../src/modules/models/workspaceCatalog.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEzE,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAA;AAEnF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,SAA0B,EAC1B,WAAmB,EACnB,MAAe;IAEf,MAAM,OAAO,GAAG,SAAS,CAAC,YAAY,EAAE,OAAO,CAAA;IAC/C,gGAAgG;IAChG,uFAAuF;IACvF,gGAAgG;IAChG,oEAAoE;IACpE,EAAE;IACF,4FAA4F;IAC5F,+FAA+F;IAC/F,gGAAgG;IAChG,wCAAwC;IACxC,MAAM,IAAI,GAAG,MAAM,4BAA4B,CAC7C;QACE,GAAG,SAAS;QACZ,uFAAuF;QACvF,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,iBAAiB;YACpC,CAAC,CAAC,EAAE,iBAAiB,EAAE,SAAS,CAAC,MAAM,CAAC,iBAAiB,EAAE;YAC3D,CAAC,CAAC,EAAE,CAAC;QACP,eAAe,EAAE,SAAS,CAAC,eAAe,EAAE,OAAO;QACnD,kBAAkB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,gBAAgB,CAAC,SAAS,CAAC,EAAE,CAAC;QACpE,oBAAoB,EAAE,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,WAAW,EAAE,SAAS,IAAI,KAAK;QACtF,GAAG,CAAC,OAAO;YACT,CAAC,CAAC,EAAE,+BAA+B,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,OAAO,CAAC,qBAAqB,CAAC,EAAE,CAAC,EAAE;YACxF,CAAC,CAAC,EAAE,CAAC;KACR,EACD,WAAW,EACX,MAAM,CACP,CAAA;IACD,MAAM,KAAK,GACT,MAAM,IAAI,SAAS,CAAC,mBAAmB;QACrC,CAAC,CAAC,MAAM,SAAS,CAAC,mBAAmB,CAAC,eAAe,CAAC,MAAM,CAAC;QAC7D,CAAC,CAAC,EAAE,CAAA;IACR,MAAM,UAAU,GAAG,SAAS,CAAC,iBAAiB;QAC5C,CAAC,CAAC,MAAM,SAAS,CAAC,iBAAiB,CAAC,eAAe,CAAC,WAAW,CAAC;QAChE,CAAC,CAAC,EAAE,CAAA;IACN,MAAM,OAAO,GAAG,iBAAiB,CAAC,iBAAiB,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAA;IACxF,OAAO,oBAAoB,CACzB,CAAC,GAAG,qBAAqB,CAAC,KAAK,CAAC,EAAE,GAAG,0BAA0B,CAAC,UAAU,CAAC,CAAC,EAC5E,IAAI,EACJ,OAAO,CACR,CAAA;AACH,CAAC"}
@@ -1,6 +1,17 @@
1
1
  import type { Context } from 'hono';
2
2
  import type { AppEnv, ServerContainer } from '../../http/env.js';
3
3
  import type { SessionPayload } from '../../auth/signing.js';
4
+ /**
5
+ * WHOSE personal credential a run may unlock — the user id and nothing else.
6
+ *
7
+ * Narrower than `SessionPayload` on purpose. A browser session is one way to establish this, and
8
+ * a public-API key its holder BOUND to themselves (`PublicApiKeyRecord.actsAsUserId`) is the
9
+ * other; asking for a whole session would have forced the second caller to fabricate the login,
10
+ * avatar and expiry of a person who is not signed in, and a fabricated session is a thing later
11
+ * code reads as one. The gate never needed more than the id: ownership of the credential is what
12
+ * it is deciding, and the PASSWORD, not this, is what proves the holder consented.
13
+ */
14
+ export type PersonalCredentialOwner = Pick<SessionPayload, 'id'>;
4
15
  /**
5
16
  * Read the ambient personal password from the request header (see
6
17
  * `PERSONAL_PASSWORD_HEADER`). The client attaches it on the gated run calls the way it
@@ -18,6 +29,23 @@ export declare function readPersonalPassword<E extends AppEnv>(c: Context<E>): s
18
29
  * so the common path never prompts. See `getCachedPassword`'s 8h expiry buffer on the client.
19
30
  */
20
31
  export declare function activateForInteraction<E extends AppEnv>(c: Context<E>, workspaceId: string, executionId: string): Promise<void>;
32
+ /**
33
+ * {@link activateForInteraction}'s context-free core, shared with the public-API decision surface
34
+ * (which resolves its user from the key's binding rather than from a session).
35
+ *
36
+ * Skips the whole gate when the run already holds a FRESH activation for every vendor it needs, and
37
+ * that short-circuit is what makes the interaction gate safe to mount in a shared preamble. Each
38
+ * re-mint derives the password's key with 210k PBKDF2 iterations per vendor, so a headless driver
39
+ * answering a run's parks one HTTP call at a time would pay that cost per call — seconds of blocked
40
+ * event loop on Node, a CPU-limit kill on workerd. `hasFreshActivation` owns the threshold, because
41
+ * only the service that mints the TTL can say what "fresh" means against it.
42
+ *
43
+ * The skip drops the password CHECK along with the derivation, and that is the honest reading rather
44
+ * than a hole: the gate exists to tell a caller to supply the password while it can still act on
45
+ * being told, and a run holding a credential that outlives its next dispatch has nothing to be told
46
+ * about. Consent was given for THIS run, by this holder, within the same activation window.
47
+ */
48
+ export declare function refreshRunActivation(container: ServerContainer, workspaceId: string, executionId: string, user: PersonalCredentialOwner | undefined, password: string | undefined): Promise<void>;
21
49
  export interface PersonalCredentialGate {
22
50
  /** Recorded on the run (individual-usage credential ownership). */
23
51
  initiatedBy: string | null;
@@ -29,9 +57,9 @@ export interface PersonalCredentialGate {
29
57
  activate?: (executionId: string) => Promise<void>;
30
58
  }
31
59
  /** Gate for STARTING a run on a block with a given pipeline. */
32
- export declare function personalGateForBlock(container: ServerContainer, workspaceId: string, blockId: string, pipelineId: string, user: SessionPayload | undefined, password: string | undefined): Promise<PersonalCredentialGate>;
60
+ export declare function personalGateForBlock(container: ServerContainer, workspaceId: string, blockId: string, pipelineId: string, user: PersonalCredentialOwner | undefined, password: string | undefined): Promise<PersonalCredentialGate>;
33
61
  /** Gate for starting a SINGLE-KIND run (the "Map service" action, the wizard's deep analysis). */
34
- export declare function personalGateForAgentKind(container: ServerContainer, workspaceId: string, blockId: string, agentKind: string, user: SessionPayload | undefined, password: string | undefined): Promise<PersonalCredentialGate>;
62
+ export declare function personalGateForAgentKind(container: ServerContainer, workspaceId: string, blockId: string, agentKind: string, user: PersonalCredentialOwner | undefined, password: string | undefined): Promise<PersonalCredentialGate>;
35
63
  /** Gate for RETRYING a failed run. */
36
- export declare function personalGateForRun(container: ServerContainer, workspaceId: string, executionId: string, user: SessionPayload | undefined, password: string | undefined): Promise<PersonalCredentialGate>;
64
+ export declare function personalGateForRun(container: ServerContainer, workspaceId: string, executionId: string, user: PersonalCredentialOwner | undefined, password: string | undefined): Promise<PersonalCredentialGate>;
37
65
  //# sourceMappingURL=personalCredentialGate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"personalCredentialGate.d.ts","sourceRoot":"","sources":["../../../src/modules/providers/personalCredentialGate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAE3D;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,SAAS,CAExF;AAmBD;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,SAAS,MAAM,EAC3D,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EACb,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CASf;AASD,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClD;AA6DD,gEAAgE;AAChE,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,cAAc,GAAG,SAAS,EAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAAC,sBAAsB,CAAC,CAiBjC;AAED,kGAAkG;AAClG,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,cAAc,GAAG,SAAS,EAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAAC,sBAAsB,CAAC,CAejC;AAED,sCAAsC;AACtC,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,cAAc,GAAG,SAAS,EAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAAC,sBAAsB,CAAC,CAcjC"}
1
+ {"version":3,"file":"personalCredentialGate.d.ts","sourceRoot":"","sources":["../../../src/modules/providers/personalCredentialGate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAE3D;;;;;;;;;GASG;AACH,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,CAAA;AAEhE;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,SAAS,CAExF;AAmBD;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,SAAS,MAAM,EAC3D,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EACb,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,uBAAuB,GAAG,SAAS,EACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAAC,IAAI,CAAC,CAKf;AAwBD,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClD;AA6DD,gEAAgE;AAChE,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,uBAAuB,GAAG,SAAS,EACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAAC,sBAAsB,CAAC,CAiBjC;AAED,kGAAkG;AAClG,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,uBAAuB,GAAG,SAAS,EACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAAC,sBAAsB,CAAC,CAejC;AAED,sCAAsC;AACtC,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,uBAAuB,GAAG,SAAS,EACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAAC,sBAAsB,CAAC,CAOjC"}
@@ -33,8 +33,39 @@ async function resolvePersonalVendorPredicate(container, user) {
33
33
  * so the common path never prompts. See `getCachedPassword`'s 8h expiry buffer on the client.
34
34
  */
35
35
  export async function activateForInteraction(c, workspaceId, executionId) {
36
- const { activate } = await personalGateForRun(c.get('container'), workspaceId, executionId, c.get('user'), readPersonalPassword(c));
37
- await activate?.(executionId);
36
+ await refreshRunActivation(c.get('container'), workspaceId, executionId, c.get('user'), readPersonalPassword(c));
37
+ }
38
+ /**
39
+ * {@link activateForInteraction}'s context-free core, shared with the public-API decision surface
40
+ * (which resolves its user from the key's binding rather than from a session).
41
+ *
42
+ * Skips the whole gate when the run already holds a FRESH activation for every vendor it needs, and
43
+ * that short-circuit is what makes the interaction gate safe to mount in a shared preamble. Each
44
+ * re-mint derives the password's key with 210k PBKDF2 iterations per vendor, so a headless driver
45
+ * answering a run's parks one HTTP call at a time would pay that cost per call — seconds of blocked
46
+ * event loop on Node, a CPU-limit kill on workerd. `hasFreshActivation` owns the threshold, because
47
+ * only the service that mints the TTL can say what "fresh" means against it.
48
+ *
49
+ * The skip drops the password CHECK along with the derivation, and that is the honest reading rather
50
+ * than a hole: the gate exists to tell a caller to supply the password while it can still act on
51
+ * being told, and a run holding a credential that outlives its next dispatch has nothing to be told
52
+ * about. Consent was given for THIS run, by this holder, within the same activation window.
53
+ */
54
+ export async function refreshRunActivation(container, workspaceId, executionId, user, password) {
55
+ const vendors = await runVendorsNeedingUnlock(container, workspaceId, executionId, user);
56
+ if (vendors.length === 0)
57
+ return;
58
+ if (user && (await holdsFreshActivations(container, executionId, user.id, vendors)))
59
+ return;
60
+ await gate(container, vendors, user, password).activate?.(executionId);
61
+ }
62
+ /** Whether every vendor the run needs already has an activation worth keeping. */
63
+ async function holdsFreshActivations(container, executionId, userId, vendors) {
64
+ const personal = container.personalSubscriptions;
65
+ if (!personal)
66
+ return false;
67
+ const fresh = await Promise.all(vendors.map((vendor) => personal.hasFreshActivation(executionId, userId, vendor)));
68
+ return fresh.every(Boolean);
38
69
  }
39
70
  /**
40
71
  * Build the gate for the set of individual-usage vendors a run will use (empty ⇒ no
@@ -101,9 +132,20 @@ export async function personalGateForAgentKind(container, workspaceId, blockId,
101
132
  }
102
133
  /** Gate for RETRYING a failed run. */
103
134
  export async function personalGateForRun(container, workspaceId, executionId, user, password) {
135
+ return gate(container, await runVendorsNeedingUnlock(container, workspaceId, executionId, user), user, password);
136
+ }
137
+ /**
138
+ * The individual-usage vendors a run's remaining steps need a MANAGED unlock for: what the engine
139
+ * resolves off the stored steps, less the ones native mode serves with the developer's own ambient
140
+ * CLI login (see {@link ambientVendors}).
141
+ *
142
+ * Extracted because two callers ask the same question and must not answer it differently: the retry
143
+ * gate, which mints a full-TTL activation for a fresh attempt, and {@link refreshRunActivation},
144
+ * which needs the set BEFORE deciding whether re-minting is necessary at all.
145
+ */
146
+ async function runVendorsNeedingUnlock(container, workspaceId, executionId, user) {
104
147
  const vendors = await container.executionService.individualVendorsForRun(workspaceId, executionId, await resolvePersonalVendorPredicate(container, user));
105
- // See personalGateForBlock: drop only the vendors native mode serves ambiently.
106
148
  const ambient = ambientVendors(container);
107
- return gate(container, vendors.filter((v) => !ambient.has(v)), user, password);
149
+ return vendors.filter((v) => !ambient.has(v));
108
150
  }
109
151
  //# sourceMappingURL=personalCredentialGate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"personalCredentialGate.js","sourceRoot":"","sources":["../../../src/modules/providers/personalCredentialGate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"personalCredentialGate.js","sourceRoot":"","sources":["../../../src/modules/providers/personalCredentialGate.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,GAEtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AAiBjE;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAmB,CAAa;IAClE,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,wBAAwB,CAAC,IAAI,SAAS,CAAA;AAC5D,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,8BAA8B,CAC3C,SAA0B,EAC1B,IAAyC;IAEzC,MAAM,QAAQ,GAAG,SAAS,CAAC,qBAAqB,CAAA;IAChD,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI;QAAE,OAAO,GAAG,EAAE,CAAC,KAAK,CAAA;IAC1C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;IAC1E,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;AACtC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,CAAa,EACb,WAAmB,EACnB,WAAmB;IAEnB,MAAM,oBAAoB,CACxB,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAClB,WAAW,EACX,WAAW,EACX,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EACb,oBAAoB,CAAC,CAAC,CAAC,CACxB,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,SAA0B,EAC1B,WAAmB,EACnB,WAAmB,EACnB,IAAyC,EACzC,QAA4B;IAE5B,MAAM,OAAO,GAAG,MAAM,uBAAuB,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,CAAC,CAAA;IACxF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAM;IAChC,IAAI,IAAI,IAAI,CAAC,MAAM,qBAAqB,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QAAE,OAAM;IAC3F,MAAM,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC,WAAW,CAAC,CAAA;AACxE,CAAC;AAED,kFAAkF;AAClF,KAAK,UAAU,qBAAqB,CAClC,SAA0B,EAC1B,WAAmB,EACnB,MAAc,EACd,OAA6B;IAE7B,MAAM,QAAQ,GAAG,SAAS,CAAC,qBAAqB,CAAA;IAChD,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3B,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAClF,CAAA;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;AAC7B,CAAC;AAoBD;;;;;;GAMG;AACH,SAAS,IAAI,CACX,SAA0B,EAC1B,OAA6B,EAC7B,IAAyC,EACzC,QAA4B;IAE5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI,EAAE,CAAA;IAClE,gFAAgF;IAChF,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAE,CAAA;IACzB,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,uBAAuB,CAC/B,oBAAoB,KAAK,yCAAyC,EAClE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAC7C,CAAA;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,SAAS,CAAC,qBAAqB,CAAA;IAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,uBAAuB,CAC/B,YAAY,KAAK,uDAAuD,EACxE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAC7C,CAAA;IACH,CAAC;IACD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,iFAAiF;QACjF,wDAAwD;QACxD,MAAM,IAAI,uBAAuB,CAAC,4CAA4C,KAAK,SAAS,EAAE;YAC5F,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,mBAAmB;SAC5B,CAAC,CAAA;IACJ,CAAC;IACD,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,EAAE;QACpB,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE;YAC9B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,MAAM,QAAQ,CAAC,cAAc,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAA;YACvE,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,cAAc,CAAC,SAA0B;IAChD,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,iBAAiB,CAAA;IAChD,OAAO,IAAI,GAAG,CAAC,wBAAwB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,qBAAqB,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;AACzF,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,SAA0B,EAC1B,WAAmB,EACnB,OAAe,EACf,UAAkB,EAClB,IAAyC,EACzC,QAA4B;IAE5B,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,gBAAgB,CAAC,yBAAyB,CACxE,WAAW,EACX,OAAO,EACP,UAAU,EACV,MAAM,8BAA8B,CAAC,SAAS,EAAE,IAAI,CAAC,CACtD,CAAA;IACD,gFAAgF;IAChF,mFAAmF;IACnF,0EAA0E;IAC1E,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;IACzC,OAAO,IAAI,CACT,SAAS,EACT,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EACtC,IAAI,EACJ,QAAQ,CACT,CAAA;AACH,CAAC;AAED,kGAAkG;AAClG,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,SAA0B,EAC1B,WAAmB,EACnB,OAAe,EACf,SAAiB,EACjB,IAAyC,EACzC,QAA4B;IAE5B,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,gBAAgB,CAAC,6BAA6B,CAC5E,WAAW,EACX,OAAO,EACP,SAAS,EACT,MAAM,8BAA8B,CAAC,SAAS,EAAE,IAAI,CAAC,CACtD,CAAA;IACD,gFAAgF;IAChF,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;IACzC,OAAO,IAAI,CACT,SAAS,EACT,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EACtC,IAAI,EACJ,QAAQ,CACT,CAAA;AACH,CAAC;AAED,sCAAsC;AACtC,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAA0B,EAC1B,WAAmB,EACnB,WAAmB,EACnB,IAAyC,EACzC,QAA4B;IAE5B,OAAO,IAAI,CACT,SAAS,EACT,MAAM,uBAAuB,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,CAAC,EACxE,IAAI,EACJ,QAAQ,CACT,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,uBAAuB,CACpC,SAA0B,EAC1B,WAAmB,EACnB,WAAmB,EACnB,IAAyC;IAEzC,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,gBAAgB,CAAC,uBAAuB,CACtE,WAAW,EACX,WAAW,EACX,MAAM,8BAA8B,CAAC,SAAS,EAAE,IAAI,CAAC,CACtD,CAAA;IACD,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;IACzC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;AAC/C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"PublicApiController.d.ts","sourceRoot":"","sources":["../../../src/modules/publicApi/PublicApiController.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAK3B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAkR/C,wBAAgB,mBAAmB,IAAI,IAAI,CAAC,MAAM,CAAC,CAWlD"}
1
+ {"version":3,"file":"PublicApiController.d.ts","sourceRoot":"","sources":["../../../src/modules/publicApi/PublicApiController.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAc3B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAmR/C,wBAAgB,mBAAmB,IAAI,IAAI,CAAC,MAAM,CAAC,CAWlD"}
@@ -4,9 +4,11 @@ import { scopeSatisfies } from '@cat-factory/integrations';
4
4
  import { buildHonoRoute } from '@toad-contracts/hono';
5
5
  import { Hono } from 'hono';
6
6
  import { streamSSE } from 'hono/streaming';
7
- import { personalGateForBlock, personalGateForRun } from '../providers/personalCredentialGate.js';
7
+ import { personalGateForBlock, personalGateForRun, } from '../providers/personalCredentialGate.js';
8
+ import { individualModelUnsupported, personalUnlockFor, unlockIsUnavailable, } from './personalUnlock.js';
8
9
  import { headlessActRefusal, notificationActEffect } from '../notifications/notificationActions.js';
9
10
  import { optionalJsonBody } from '../../http/optionalJsonBody.js';
11
+ import { keyInitiatorRole } from '../../http/runAdmission.js';
10
12
  import { authorize } from './publicApiAuth.js';
11
13
  import { toPublicService, toPublicTask } from './boardProjection.js';
12
14
  import { createTaskWithAttachments, taskCreationDeps } from './taskCreation.js';
@@ -307,30 +309,48 @@ function registerJobRoutes(app) {
307
309
  },
308
310
  }, 429);
309
311
  }
312
+ // What this call may unlock, read before anything is written so the refusal below and the
313
+ // attribution above are decided from one answer.
314
+ const unlock = personalUnlockFor(c, auth);
310
315
  const block = await container.boardService.createInternalTask(auth.workspaceId, {
311
316
  title: title?.trim() || input.slice(0, 80),
312
317
  description: input,
313
318
  });
314
- // Headless / system-initiated: no `usr_*` initiator (an inline public run never leases a
315
- // personal credential), so pass null rather than a synthetic user id. If start fails, roll the
316
- // whole run back: `ExecutionService.start` persists the execution + live-run row and flips the
317
- // block to `in_progress` BEFORE its throwing dispatch (`workRunner.startRun`), so deleting only
318
- // the anchor block would orphan a `running` execution the stale-run sweeper then re-drives
319
- // forever against a since-deleted block. `rollbackInitiativeRun` drops the execution first, then
320
- // the block, so a failed dispatch leaves nothing behind (whether it threw before or after the
321
- // rows were written).
319
+ // If anything below fails, roll the whole run back: `ExecutionService.start` persists the
320
+ // execution + live-run row and flips the block to `in_progress` BEFORE its throwing dispatch
321
+ // (`workRunner.startRun`), so deleting only the anchor block would orphan a `running` execution
322
+ // the stale-run sweeper then re-drives forever against a since-deleted block.
323
+ // `rollbackInitiativeRun` drops the execution first, then the block, so a failed start leaves
324
+ // nothing behind (whether it threw before or after the rows were written) — and the gate below
325
+ // throws before either exists, which the same rollback covers.
322
326
  let execution;
323
327
  try {
328
+ // The SAME personal-credential gate the board-start route runs, on the same primitive. An
329
+ // inline-only pipeline settles what a public run may DO (no container, no push); it says
330
+ // nothing about WHOSE credential the work needs, because an inline step leases a personal
331
+ // subscription like any other — the inline harness's `makeContainerRunner` leases one for
332
+ // every individual-usage vendor. Skipping the gate here would trade a refusal the caller can
333
+ // act on for a run that dies at its first dispatch with nothing to lease.
334
+ const personal = await personalGateForBlock(container, auth.workspaceId, block.id, pipelineId, unlock.user, unlock.password);
324
335
  execution = await container.executionService.start(auth.workspaceId, block.id, pipelineId, {
325
- initiatedBy: null,
326
- // No `usr_*` initiator, but the KEY may still name who it acts for. Pinned on the run so
327
- // a caller minting one key per person can map this job back to that person later.
336
+ // The bound user, or `null` for an unbound key (headless / system-initiated, as before).
337
+ initiatedBy: personal.initiatedBy,
338
+ // Whose policy the run is admitted under: the bound user's own tier, so a headless start
339
+ // cannot land what that person could not land from the board. See `keyInitiatorRole`.
340
+ initiatedByRole: await keyInitiatorRole(container, auth.workspaceId, personal.initiatedBy),
341
+ // Beside the initiator, never instead of it: the KEY may still name who it acts for on
342
+ // its provisioner's side. Pinned on the run so a caller minting one key per person can
343
+ // map this job back to that person later, whether or not the platform knows them.
328
344
  initiatedByExternalIdentity: auth.externalIdentity,
329
345
  intakeOrigin: 'public-api',
346
+ activate: personal.activate,
330
347
  });
331
348
  }
332
349
  catch (err) {
333
350
  await rollbackInitiativeRun(c, auth.workspaceId, block.id);
351
+ if (err instanceof CredentialRequiredError && unlockIsUnavailable(unlock)) {
352
+ return c.json({ error: individualModelUnsupported('job') }, 409);
353
+ }
334
354
  throw err;
335
355
  }
336
356
  // Close the check-then-act race on the cap. The pre-check above is a fast path, but it can let
@@ -664,34 +684,43 @@ function registerTaskRoutes(app) {
664
684
  if (taskParkRefusal) {
665
685
  return c.json({ error: { code: 'pipeline_requires_decide_scope', message: taskParkRefusal } }, 403);
666
686
  }
667
- // A headless key has no user/password to unlock a personal (individual-usage)
668
- // subscription, so refuse a task whose model resolves to such a vendor (Claude / Codex)
669
- // up front. The gate throws `CredentialRequiredError` (→ 428) for exactly that case; it
670
- // is a no-op for an ordinary poolable model. Passing no user means only subscription-ONLY
671
- // vendors gate (a dual-mode GLM task still runs on the poolable Cloudflare base).
687
+ // A task whose model resolves to an individual-usage vendor (Claude / Codex) needs that
688
+ // person's own subscription unlocked. An UNBOUND key has nobody to unlock for, so the gate
689
+ // throws and the refusal below states the unsupported case; a key its holder bound to
690
+ // themselves supplies both halves and the gate mints the run's activation like any other
691
+ // start. A no-op either way for an ordinary poolable model, and passing no user means only
692
+ // subscription-ONLY vendors gate (a dual-mode GLM task still runs on the poolable base).
693
+ const unlock = personalUnlockFor(c, auth);
694
+ let personal;
672
695
  try {
673
- await personalGateForBlock(container, auth.workspaceId, taskId, pipelineId, undefined, undefined);
696
+ personal = await personalGateForBlock(container, auth.workspaceId, taskId, pipelineId, unlock.user, unlock.password);
674
697
  }
675
698
  catch (err) {
676
- if (err instanceof CredentialRequiredError) {
677
- return c.json({
678
- error: {
679
- code: 'individual_model_unsupported',
680
- message: 'This task runs on an individual-usage model that needs an interactive personal-credential unlock; it cannot be started through the API',
681
- },
682
- }, 409);
699
+ if (err instanceof CredentialRequiredError && unlockIsUnavailable(unlock)) {
700
+ return c.json({ error: individualModelUnsupported('task') }, 409);
683
701
  }
702
+ // A bound key CAN answer this: let the 428 through with its vendor + reason so the caller
703
+ // knows to prompt for (or correct) the password rather than treating it as unsupported.
684
704
  throw err;
685
705
  }
686
- // Headless / system-initiated: no `usr_*` initiator. The engine's own start-time gates
687
- // (per-service running-task cap, dependency gate, runnability) apply as for any board start;
688
- // their `DomainError`s map to the right HTTP status via the shared error handler. This is the
689
- // abuse backstop for board starts — the analogue of the jobs surface's active-run cap.
706
+ // The engine's own start-time gates (per-service running-task cap, dependency gate,
707
+ // runnability) apply as for any board start; their `DomainError`s map to the right HTTP status
708
+ // via the shared error handler. This is the abuse backstop for board starts — the analogue of
709
+ // the jobs surface's active-run cap.
690
710
  await container.executionService.start(auth.workspaceId, taskId, pipelineId, {
691
- initiatedBy: null,
692
- // As on the jobs surface: no user, but the key can still name who it started this for.
711
+ // The bound user, or `null` for an unbound key (headless / system-initiated, as before).
712
+ initiatedBy: personal.initiatedBy,
713
+ // And the tier that user holds on this board, so a bound key's run is admitted under the
714
+ // same merge policy, submission allow-list and dry-run sandbox its holder gets in the app.
715
+ // See `keyInitiatorRole`: an initiator with no role is a run with no policy, not a lenient
716
+ // one.
717
+ initiatedByRole: await keyInitiatorRole(container, auth.workspaceId, personal.initiatedBy),
718
+ // As on the jobs surface: the key can still name who it started this for, and it does so
719
+ // whether or not a user is bound — the two answer different questions (a `usr_*` account
720
+ // here, an identity only the caller's own side can resolve there).
693
721
  initiatedByExternalIdentity: auth.externalIdentity,
694
722
  intakeOrigin: 'public-api',
723
+ activate: personal.activate,
695
724
  });
696
725
  // Re-read the task so the caller gets its AUTHORITATIVE post-start projection (status,
697
726
  // executionId, progress) rather than an optimistic guess — a run may park/block at its first
@@ -794,24 +823,31 @@ function registerTaskLifecycleRoutes(app) {
794
823
  if (!run) {
795
824
  return c.json({ error: { code: 'no_run', message: 'Task has no run to retry' } }, 409);
796
825
  }
826
+ const unlock = personalUnlockFor(c, auth);
827
+ let personal;
797
828
  try {
798
- await personalGateForRun(container, auth.workspaceId, run.id, undefined, undefined);
829
+ personal = await personalGateForRun(container, auth.workspaceId, run.id, unlock.user, unlock.password);
799
830
  }
800
831
  catch (err) {
801
- if (err instanceof CredentialRequiredError) {
802
- return c.json({
803
- error: {
804
- code: 'individual_model_unsupported',
805
- message: 'This task runs on an individual-usage model that needs an interactive personal-credential unlock; it cannot be retried through the API',
806
- },
807
- }, 409);
832
+ if (err instanceof CredentialRequiredError && unlockIsUnavailable(unlock)) {
833
+ return c.json({ error: individualModelUnsupported('task') }, 409);
808
834
  }
809
835
  throw err;
810
836
  }
811
- // Headless / system-initiated: no `usr_*` initiator and no personal-credential activation
812
- // (the gate above already refused the only case that would need one). A non-failed run is
813
- // rejected inside `retry` with `run_not_retryable` 409 via the shared error handler.
814
- await container.executionService.retry(auth.workspaceId, run.id, null, undefined);
837
+ // A non-failed run is rejected inside `retry` with `run_not_retryable` 409 via the shared
838
+ // error handler. The retry re-drives the STORED steps, so the activation the gate minted (if
839
+ // any) is the one those steps will lease.
840
+ //
841
+ // No role is pinned here, and that asymmetry with the start routes above is the engine's rule
842
+ // rather than an omission. A bound key that retries someone else's failed run BECOMES its
843
+ // initiator, because only the person present can unlock their own subscription — but the
844
+ // ADMISSION authority does not move with them: `buildResumedInstance` carries the pinned role
845
+ // and mode forward from the run being replaced, so a re-drive stays the same work under the
846
+ // authority it was originally granted. Re-resolving it for whoever drove the retry is what
847
+ // would be wrong (a sweeper drives one with no user at all), and dropping it is the escape
848
+ // hatch that comment names: restart-from-step-0 would launder a dry run into a live one. The
849
+ // SPA's retry path (`AgentRunController`) has exactly this shape, for exactly this reason.
850
+ await container.executionService.retry(auth.workspaceId, run.id, personal.initiatedBy, personal.activate);
815
851
  const after = await container.boardService.getServiceTask(auth.workspaceId, taskId);
816
852
  const projected = after ?? found;
817
853
  return c.json(toPublicTask(projected.block, projected.service.id), 202);
@@ -1044,6 +1080,12 @@ function registerNotificationRoutes(app) {
1044
1080
  // run resolves to an individual-usage model (the same `personalGateForRun` primitive the
1045
1081
  // retry route uses). A no-op for a poolable model, and skipped entirely for a card whose
1046
1082
  // side-effect is a merge (no personal credential needed) or that is already resolved.
1083
+ //
1084
+ // Probed with NO unlock even when the key carries one, unlike the start and retry routes.
1085
+ // `notificationActEffect` is a shared side-effect whose retry arm mints no activation (the
1086
+ // SPA's card path has the same shape), so admitting a bound key here would trade a refusal
1087
+ // the caller can act on for a run that fails at its first dispatch with nothing to lease.
1088
+ // Lifting it means threading the gate through that effect, for both surfaces at once.
1047
1089
  if (existing.status === 'open' &&
1048
1090
  (existing.type === 'ci_failed' || existing.type === 'test_failed') &&
1049
1091
  existing.executionId) {