@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.
- package/dist/agents/providerCapabilities.d.ts +14 -1
- package/dist/agents/providerCapabilities.d.ts.map +1 -1
- package/dist/agents/providerCapabilities.js +10 -1
- package/dist/agents/providerCapabilities.js.map +1 -1
- package/dist/http/runAdmission.d.ts +27 -1
- package/dist/http/runAdmission.d.ts.map +1 -1
- package/dist/http/runAdmission.js +32 -0
- package/dist/http/runAdmission.js.map +1 -1
- package/dist/modules/models/workspaceCatalog.d.ts.map +1 -1
- package/dist/modules/models/workspaceCatalog.js +4 -0
- package/dist/modules/models/workspaceCatalog.js.map +1 -1
- package/dist/modules/providers/personalCredentialGate.d.ts +31 -3
- package/dist/modules/providers/personalCredentialGate.d.ts.map +1 -1
- package/dist/modules/providers/personalCredentialGate.js +46 -4
- package/dist/modules/providers/personalCredentialGate.js.map +1 -1
- package/dist/modules/publicApi/PublicApiController.d.ts.map +1 -1
- package/dist/modules/publicApi/PublicApiController.js +85 -43
- package/dist/modules/publicApi/PublicApiController.js.map +1 -1
- package/dist/modules/publicApi/PublicApiKeyController.d.ts.map +1 -1
- package/dist/modules/publicApi/PublicApiKeyController.js +18 -2
- package/dist/modules/publicApi/PublicApiKeyController.js.map +1 -1
- package/dist/modules/publicApi/PublicKeyController.d.ts.map +1 -1
- package/dist/modules/publicApi/PublicKeyController.js +6 -0
- package/dist/modules/publicApi/PublicKeyController.js.map +1 -1
- package/dist/modules/publicApi/PublicProvisioningController.d.ts.map +1 -1
- package/dist/modules/publicApi/PublicProvisioningController.js +33 -11
- package/dist/modules/publicApi/PublicProvisioningController.js.map +1 -1
- package/dist/modules/publicApi/decisions/scope.d.ts +10 -1
- package/dist/modules/publicApi/decisions/scope.d.ts.map +1 -1
- package/dist/modules/publicApi/decisions/scope.js +59 -2
- package/dist/modules/publicApi/decisions/scope.js.map +1 -1
- package/dist/modules/publicApi/keyProjection.d.ts.map +1 -1
- package/dist/modules/publicApi/keyProjection.js +1 -0
- package/dist/modules/publicApi/keyProjection.js.map +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.d.ts +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.d.ts.map +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.js +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.js.map +1 -1
- package/dist/modules/publicApi/personalUnlock.d.ts +51 -0
- package/dist/modules/publicApi/personalUnlock.d.ts.map +1 -0
- package/dist/modules/publicApi/personalUnlock.js +45 -0
- package/dist/modules/publicApi/personalUnlock.js.map +1 -0
- 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,
|
|
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":"AAQA,OAAO,EACL,wBAAwB,
|
|
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;
|
|
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;
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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":"AAAA,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,GAEtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"personalCredentialGate.js","sourceRoot":"","sources":["../../../src/modules/providers/personalCredentialGate.ts"],"names":[],"mappings":"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"}
|
|
@@ -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;
|
|
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
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
//
|
|
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
|
|
668
|
-
// subscription
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
//
|
|
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,
|
|
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
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
//
|
|
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
|
-
|
|
692
|
-
|
|
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,
|
|
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
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
|
|
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) {
|