@intx/hub-sessions 0.2.2 → 0.4.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/README.md +3 -5
- package/dist/agent-repo.d.ts +23 -7
- package/dist/agent-repo.js +19 -6
- package/dist/agent-state-kind.js +18 -63
- package/dist/asset-service.d.ts +1 -20
- package/dist/asset-service.js +19 -97
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +52 -7
- package/dist/credential-push.js +178 -22
- package/dist/event-collector-registry.d.ts +3 -2
- package/dist/event-collector-registry.js +42 -13
- package/dist/event-collector.d.ts +12 -2
- package/dist/event-collector.js +45 -4
- package/dist/hub-session-lookups.d.ts +125 -7
- package/dist/hub-session-lookups.js +541 -86
- package/dist/hub-session-orchestrator.d.ts +2 -3
- package/dist/hub-session-orchestrator.js +22 -56
- package/dist/index.d.ts +19 -9
- package/dist/index.js +16 -7
- package/dist/reconciliation-scheduler.d.ts +14 -0
- package/dist/reconciliation-scheduler.js +55 -0
- package/dist/repo-store/index.d.ts +2 -1
- package/dist/repo-store/index.js +1 -0
- package/dist/repo-store/store.d.ts +1 -1
- package/dist/repo-store/store.js +138 -1
- package/dist/repo-store/subscribe-kind.d.ts +6 -3
- package/dist/repo-store/subscribe-kind.js +42 -77
- package/dist/repo-store/types.d.ts +94 -6
- package/dist/repo-store/user-principal-gate.d.ts +26 -0
- package/dist/repo-store/user-principal-gate.js +78 -0
- package/dist/session-service.d.ts +252 -126
- package/dist/session-service.js +851 -624
- package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
- package/dist/sidecar-allocation/capability-policy.js +124 -0
- package/dist/sidecar-allocation/contracts.d.ts +101 -0
- package/dist/sidecar-allocation/contracts.js +26 -0
- package/dist/sidecar-allocation/index.d.ts +5 -0
- package/dist/sidecar-allocation/index.js +4 -0
- package/dist/sidecar-allocation/operation.d.ts +10 -0
- package/dist/sidecar-allocation/operation.js +54 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
- package/dist/sidecar-allocation/plugin-registry.js +61 -0
- package/dist/sidecar-allocation/reconciler.d.ts +54 -0
- package/dist/sidecar-allocation/reconciler.js +825 -0
- package/dist/skill-kind.js +12 -62
- package/dist/substrate.d.ts +3 -3
- package/dist/substrate.js +1 -1
- package/dist/workflow-allocation-service.d.ts +64 -0
- package/dist/workflow-allocation-service.js +554 -0
- package/dist/workflow-closure-resolution.d.ts +106 -0
- package/dist/workflow-closure-resolution.js +123 -0
- package/dist/workflow-definition-ensure.d.ts +24 -0
- package/dist/workflow-definition-ensure.js +75 -0
- package/dist/workflow-dispatch-service.d.ts +42 -0
- package/dist/workflow-dispatch-service.js +209 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +29 -1
- package/dist/workflow-kind.js +143 -139
- package/dist/workflow-probe-gate.d.ts +286 -0
- package/dist/workflow-probe-gate.js +382 -0
- package/dist/workflow-run-kind.d.ts +239 -32
- package/dist/workflow-run-kind.js +874 -188
- package/dist/workflow-run-reader.d.ts +1 -1
- package/dist/workflow-run-reader.js +3 -7
- package/dist/workflow-run-restore.d.ts +16 -0
- package/dist/workflow-run-restore.js +30 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/workflow-source-pins.d.ts +8 -0
- package/dist/workflow-source-pins.js +14 -0
- package/dist/ws/index.d.ts +3 -3
- package/dist/ws/index.js +2 -2
- package/dist/ws/pending-tracker.d.ts +93 -0
- package/dist/ws/pending-tracker.js +132 -0
- package/dist/ws/sidecar-events.d.ts +139 -37
- package/dist/ws/sidecar-events.js +2 -2
- package/dist/ws/sidecar-handler.d.ts +218 -60
- package/dist/ws/sidecar-handler.js +1713 -732
- package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
- package/dist/ws/sidecar-handler.test-helpers.js +95 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
- package/dist/ws/sidecar-token-authenticator.js +78 -7
- package/package.json +14 -13
- package/dist/available-skills-stanza.d.ts +0 -21
- package/dist/available-skills-stanza.js +0 -32
package/README.md
CHANGED
|
@@ -95,11 +95,9 @@ for the authoritative type shapes.
|
|
|
95
95
|
### Asset service
|
|
96
96
|
|
|
97
97
|
- `createAssetService` / `AssetService`, `AssetServiceError`,
|
|
98
|
-
`DEFAULT_ASSET_REF`, and the supporting types `Asset`,
|
|
99
|
-
`
|
|
100
|
-
`
|
|
101
|
-
`AssetServiceErrorReason`, `ReadAssetBlobParams`,
|
|
102
|
-
`ListAssetBlobsParams`.
|
|
98
|
+
`DEFAULT_ASSET_REF`, and the supporting types `Asset`,
|
|
99
|
+
`CreateAssetParams`, `PopulateAssetParams`, `AssetServiceErrorReason`,
|
|
100
|
+
`ReadAssetBlobParams`, `ListAssetBlobsParams`.
|
|
103
101
|
|
|
104
102
|
### Skills stanza and credential push
|
|
105
103
|
|
package/dist/agent-repo.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { GCPolicy } from "@intx/storage-isogit";
|
|
1
|
+
import type { GCPolicy } from "@intx/storage-isogit/node";
|
|
2
2
|
import type { ToolPackageManifest } from "@intx/types/tool-packages";
|
|
3
|
-
import type { RepoId, RepoStore } from "./repo-store/index.js";
|
|
3
|
+
import type { NewlyTerminalRun, RepoId, RepoStore } from "./repo-store/index.js";
|
|
4
4
|
export type DeployContent = {
|
|
5
5
|
systemPrompt: string;
|
|
6
6
|
/**
|
|
@@ -44,7 +44,7 @@ export type AgentRepoStore = {
|
|
|
44
44
|
* tree.
|
|
45
45
|
*
|
|
46
46
|
* `repoId.kind` must be `"agent-state"`. The `repoId.id` is used as
|
|
47
|
-
* the
|
|
47
|
+
* the run address internally. Stamps an
|
|
48
48
|
* `AgentStateSidecarPrincipal` so the agent-state kind handler
|
|
49
49
|
* authorizes the receivePack as a per-agent sidecar write.
|
|
50
50
|
*/
|
|
@@ -55,15 +55,17 @@ export type AgentRepoStore = {
|
|
|
55
55
|
* tree.
|
|
56
56
|
*
|
|
57
57
|
* `repoId.kind` must be `"workflow-run"`. Stamps a
|
|
58
|
-
* `WorkflowRunSupervisorPrincipal` whose `
|
|
58
|
+
* `WorkflowRunSupervisorPrincipal` whose `anchorRunId` equals
|
|
59
59
|
* `repoId.id`; the workflow-run kind handler denies sidecar-kind
|
|
60
60
|
* writes by design, so the supervisor branch is the correct path for
|
|
61
61
|
* sidecar-originated run-event commits (the supervisor lives on the
|
|
62
62
|
* sidecar and is the actual signer of the run events).
|
|
63
|
+
*
|
|
64
|
+
* Returns the runs the pack drove terminal, surfaced from the
|
|
65
|
+
* substrate's per-commit validation so a caller can flip the run's
|
|
66
|
+
* DB state without re-deriving terminal-ness.
|
|
63
67
|
*/
|
|
64
|
-
receiveWorkflowRunPack(repoId: RepoId, pack: Uint8Array, ref: string, commitSha: string): Promise<
|
|
65
|
-
/** Resolve the current deploy ref SHA, or null if no deploy exists. */
|
|
66
|
-
getDeployRef(agentId: string): Promise<string | null>;
|
|
68
|
+
receiveWorkflowRunPack(repoId: RepoId, pack: Uint8Array, ref: string, commitSha: string): Promise<NewlyTerminalRun[]>;
|
|
67
69
|
/** Raw 32-byte Ed25519 public key used to sign deploy commits. */
|
|
68
70
|
getSigningPublicKey(): Uint8Array;
|
|
69
71
|
/**
|
|
@@ -74,6 +76,20 @@ export type AgentRepoStore = {
|
|
|
74
76
|
*/
|
|
75
77
|
readonly repoStore: RepoStore;
|
|
76
78
|
};
|
|
79
|
+
/**
|
|
80
|
+
* Repo kinds eligible for write-path object GC. Deliberately EXCLUDES
|
|
81
|
+
* "workflow-run". The warm-agent mailbox physically expunges `<uid>.eml`
|
|
82
|
+
* blobs from the live tree; the raw bytes then persist only through the
|
|
83
|
+
* parent commit in git history, and they survive a push ONLY because a
|
|
84
|
+
* `workflow-run` repo's objects are never GC'd. Adding "workflow-run"
|
|
85
|
+
* here -- above all with a retention other than "keep-history" -- would
|
|
86
|
+
* make an expunged message's bytes prunable and silently destroy the
|
|
87
|
+
* mailbox audit trail. The mailbox subtree contract in `workflow-run-kind`
|
|
88
|
+
* documents this dependency; `agent-repo.test.ts` pins it. Do not add
|
|
89
|
+
* "workflow-run" without replacing physical expunge with a tip-reachable
|
|
90
|
+
* retain-bytes scheme first.
|
|
91
|
+
*/
|
|
92
|
+
export declare const DEFAULT_GC_KINDS: readonly ["agent-state"];
|
|
77
93
|
export declare function createAgentRepoStore(config: {
|
|
78
94
|
dataDir: string;
|
|
79
95
|
signingKey: {
|
package/dist/agent-repo.js
CHANGED
|
@@ -5,6 +5,20 @@ import { skillKindHandler, skillAuthorize } from "./skill-kind.js";
|
|
|
5
5
|
import { packageRegistryKindHandler, packageRegistryAuthorize, } from "./package-registry-kind.js";
|
|
6
6
|
import { workflowKindHandler, workflowAuthorize } from "./workflow-kind.js";
|
|
7
7
|
import { workflowRunKindHandler, workflowRunAuthorize, } from "./workflow-run-kind.js";
|
|
8
|
+
/**
|
|
9
|
+
* Repo kinds eligible for write-path object GC. Deliberately EXCLUDES
|
|
10
|
+
* "workflow-run". The warm-agent mailbox physically expunges `<uid>.eml`
|
|
11
|
+
* blobs from the live tree; the raw bytes then persist only through the
|
|
12
|
+
* parent commit in git history, and they survive a push ONLY because a
|
|
13
|
+
* `workflow-run` repo's objects are never GC'd. Adding "workflow-run"
|
|
14
|
+
* here -- above all with a retention other than "keep-history" -- would
|
|
15
|
+
* make an expunged message's bytes prunable and silently destroy the
|
|
16
|
+
* mailbox audit trail. The mailbox subtree contract in `workflow-run-kind`
|
|
17
|
+
* documents this dependency; `agent-repo.test.ts` pins it. Do not add
|
|
18
|
+
* "workflow-run" without replacing physical expunge with a tip-reachable
|
|
19
|
+
* retain-bytes scheme first.
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_GC_KINDS = ["agent-state"];
|
|
8
22
|
export function createAgentRepoStore(config) {
|
|
9
23
|
const { dataDir, signingKey, gc } = config;
|
|
10
24
|
const authorize = (principal, incomingRepoId, ref, action) => {
|
|
@@ -47,7 +61,9 @@ export function createAgentRepoStore(config) {
|
|
|
47
61
|
},
|
|
48
62
|
authorize,
|
|
49
63
|
signingCallback: () => signer,
|
|
50
|
-
...(gc === undefined
|
|
64
|
+
...(gc === undefined
|
|
65
|
+
? {}
|
|
66
|
+
: { gc: { kinds: [...DEFAULT_GC_KINDS], ...gc } }),
|
|
51
67
|
});
|
|
52
68
|
const hub = { kind: "hub" };
|
|
53
69
|
function repoId(agentId) {
|
|
@@ -93,13 +109,10 @@ export function createAgentRepoStore(config) {
|
|
|
93
109
|
}
|
|
94
110
|
const principal = {
|
|
95
111
|
kind: "supervisor",
|
|
96
|
-
|
|
112
|
+
anchorRunId: incomingRepoId.id,
|
|
97
113
|
};
|
|
98
114
|
const expectedOldSha = await store.resolveRef(hub, incomingRepoId, ref);
|
|
99
|
-
|
|
100
|
-
},
|
|
101
|
-
async getDeployRef(agentId) {
|
|
102
|
-
return store.resolveRef(hub, repoId(agentId), AGENT_STATE_DEPLOY_REF);
|
|
115
|
+
return store.receivePack(principal, incomingRepoId, ref, pack, commitSha, expectedOldSha);
|
|
103
116
|
},
|
|
104
117
|
getSigningPublicKey() {
|
|
105
118
|
return signingKey.publicKey;
|
package/dist/agent-state-kind.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type } from "arktype";
|
|
2
|
-
import {
|
|
3
|
-
import { UserPrincipal, } from "./repo-store/index.js";
|
|
2
|
+
import { authorizeUserPrincipal, } from "./repo-store/index.js";
|
|
4
3
|
const SidecarPrincipal = type({
|
|
5
4
|
kind: "'sidecar'",
|
|
6
5
|
agentId: "string",
|
|
@@ -62,10 +61,9 @@ export const agentStateKindHandler = {
|
|
|
62
61
|
};
|
|
63
62
|
export const agentStateAuthorize = (principal, repoId, ref, action) => {
|
|
64
63
|
if (principal.kind === "hub") {
|
|
65
|
-
// Full access at this kind. Hub-side reads (
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// places.
|
|
64
|
+
// Full access at this kind. Hub-side reads (createDeployPack) and writes
|
|
65
|
+
// (writeDeployTree) all flow through here, so changing this branch tightens
|
|
66
|
+
// behavior in non-obvious places.
|
|
69
67
|
return { allowed: true };
|
|
70
68
|
}
|
|
71
69
|
if (principal.kind === "sidecar") {
|
|
@@ -114,70 +112,27 @@ export const agentStateAuthorize = (principal, repoId, ref, action) => {
|
|
|
114
112
|
}
|
|
115
113
|
}
|
|
116
114
|
if (principal.kind === "user") {
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// bound the requested (ref, action) and have not expired, and
|
|
121
|
-
// (b) sanity-checks that the pre-resolved verdict targets this
|
|
122
|
-
// exact resource and grant verb. Both gates must pass before the
|
|
123
|
-
// verdict's `effect` is honoured.
|
|
124
|
-
const parsed = UserPrincipal(principal);
|
|
125
|
-
if (parsed instanceof type.errors) {
|
|
126
|
-
return {
|
|
127
|
-
allowed: false,
|
|
128
|
-
reason: `user principal is malformed: ${parsed.summary}`,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
115
|
+
// `agentStateAuthorize` has no top-level repoId.kind gate (the
|
|
116
|
+
// other kind handlers reject a mismatched kind up front), so the
|
|
117
|
+
// kind check lives here inside the user branch.
|
|
131
118
|
if (repoId.kind !== "agent-state") {
|
|
132
119
|
return {
|
|
133
120
|
allowed: false,
|
|
134
121
|
reason: `user authorize received non-agent-state repo ${repoId.kind}/${repoId.id}`,
|
|
135
122
|
};
|
|
136
123
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// performed by `listRefs`. Per-ref filtering is the advertise-refs
|
|
145
|
-
// layer's responsibility, so the bulk read is gated on action and
|
|
146
|
-
// expiry alone.
|
|
147
|
-
if (ref !== "*" && !glob.match(parsed.tokenClaims.refPattern, ref)) {
|
|
148
|
-
return {
|
|
149
|
-
allowed: false,
|
|
150
|
-
reason: `token refPattern ${parsed.tokenClaims.refPattern} does not match ${ref}`,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
if (Date.now() >= parsed.tokenClaims.expiresAt) {
|
|
154
|
-
return {
|
|
155
|
-
allowed: false,
|
|
156
|
-
reason: `token expired at ${parsed.tokenClaims.expiresAt}`,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const expectedResource = `agent-state:${repoId.id}`;
|
|
160
|
-
if (parsed.authz.resource !== expectedResource) {
|
|
161
|
-
return {
|
|
162
|
-
allowed: false,
|
|
163
|
-
reason: `authz verdict resource ${parsed.authz.resource} does not match ${expectedResource}`,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
const expectedGrantVerb = repoActionToGrantVerb(action);
|
|
167
|
-
if (parsed.authz.grantVerb !== expectedGrantVerb) {
|
|
168
|
-
return {
|
|
169
|
-
allowed: false,
|
|
170
|
-
reason: `authz verdict grantVerb ${parsed.authz.grantVerb} does not match ${expectedGrantVerb}`,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
if (parsed.authz.effect === "allow") {
|
|
174
|
-
return { allowed: true };
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
allowed: false,
|
|
178
|
-
reason: `authz verdict denied for ${expectedResource} ${expectedGrantVerb}`,
|
|
179
|
-
};
|
|
124
|
+
return authorizeUserPrincipal({
|
|
125
|
+
principal,
|
|
126
|
+
repoId,
|
|
127
|
+
ref,
|
|
128
|
+
action,
|
|
129
|
+
resourcePrefix: "agent-state",
|
|
130
|
+
});
|
|
180
131
|
}
|
|
132
|
+
// Fail closed on any kind not handled above. The tenant-level
|
|
133
|
+
// `workflow` principal kind (`@intx/types` principalKinds) is a
|
|
134
|
+
// grant owner, not an agent-state bearer, and never carries an
|
|
135
|
+
// agent-state repo push here -- so it is intentionally left denied.
|
|
181
136
|
return {
|
|
182
137
|
allowed: false,
|
|
183
138
|
reason: `unknown principal kind: ${principal.kind}`,
|
package/dist/asset-service.d.ts
CHANGED
|
@@ -11,18 +11,6 @@ export type Asset = {
|
|
|
11
11
|
createdAt: Date;
|
|
12
12
|
updatedAt: Date;
|
|
13
13
|
};
|
|
14
|
-
export type AccessMode = "read-only" | "read-write";
|
|
15
|
-
export type AgentAsset = {
|
|
16
|
-
id: string;
|
|
17
|
-
agentId: string;
|
|
18
|
-
assetId: string;
|
|
19
|
-
ref: string;
|
|
20
|
-
accessMode: AccessMode;
|
|
21
|
-
createdAt: Date;
|
|
22
|
-
};
|
|
23
|
-
export type AgentAssetWithAsset = AgentAsset & {
|
|
24
|
-
asset: Pick<Asset, "id" | "tenantId" | "kind" | "name" | "displayName">;
|
|
25
|
-
};
|
|
26
14
|
export type CreateAssetParams = {
|
|
27
15
|
tenantId: string;
|
|
28
16
|
/** Accepted kinds: "skill", "package-registry", "workflow".
|
|
@@ -48,19 +36,11 @@ export type PopulateAssetParams = {
|
|
|
48
36
|
* (e.g. skillAuthorize only permits `kind: "hub"` writes). */
|
|
49
37
|
principal: Principal;
|
|
50
38
|
};
|
|
51
|
-
export type AttachAssetParams = {
|
|
52
|
-
agentId: string;
|
|
53
|
-
assetId: string;
|
|
54
|
-
ref: string;
|
|
55
|
-
accessMode?: AccessMode;
|
|
56
|
-
};
|
|
57
39
|
export interface AssetService {
|
|
58
40
|
createAsset(params: CreateAssetParams): Promise<Asset>;
|
|
59
41
|
populateAsset(params: PopulateAssetParams): Promise<{
|
|
60
42
|
commitSha: string;
|
|
61
43
|
}>;
|
|
62
|
-
attachAsset(params: AttachAssetParams): Promise<AgentAsset>;
|
|
63
|
-
listAgentAssets(agentId: string): Promise<AgentAssetWithAsset[]>;
|
|
64
44
|
/**
|
|
65
45
|
* In-process blob read. Resolves the asset's row, then reads the blob
|
|
66
46
|
* at `path` from the commit pointed to by `ref` (defaults to
|
|
@@ -99,6 +79,7 @@ export declare const DEFAULT_ASSET_REF = "refs/heads/main";
|
|
|
99
79
|
/** Discriminator for AssetServiceError variants. Lets callers branch
|
|
100
80
|
* without instanceof gymnastics across the different error subclasses. */
|
|
101
81
|
export type AssetServiceErrorReason = "unsupported_kind" | "duplicate_asset" | "duplicate_attachment" | "invalid_name" | "invalid_reference" | "name_reserved" | "not_found" | "path_violation";
|
|
82
|
+
export declare const ASSET_NAME_PATTERN: RegExp;
|
|
102
83
|
export declare class AssetServiceError extends Error {
|
|
103
84
|
readonly reason: AssetServiceErrorReason;
|
|
104
85
|
constructor(reason: AssetServiceErrorReason, message: string, cause?: unknown);
|
package/dist/asset-service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// In-process service for creating and
|
|
1
|
+
// In-process service for creating and populating skill-asset repos.
|
|
2
2
|
//
|
|
3
3
|
// Three responsibilities are layered here, mirroring the substrate's
|
|
4
4
|
// own layering (DB row, repo bookkeeping, content validation):
|
|
@@ -8,19 +8,15 @@
|
|
|
8
8
|
// populateAsset drives RepoStore.writeTree, which runs the kind
|
|
9
9
|
// handler's validatePush before advancing the ref.
|
|
10
10
|
// Content rejections surface as AssetValidationError.
|
|
11
|
-
// attachAsset inserts an agent_asset row, surfacing the
|
|
12
|
-
// (agentId, assetId) uniqueness violation (which
|
|
13
|
-
// prevents the same asset being attached to one
|
|
14
|
-
// agent twice) as AssetAttachError.
|
|
15
11
|
//
|
|
16
12
|
// The factory is closure-based to match createAgentRepoStore and
|
|
17
13
|
// createRepoStore. There is no class because there is no per-instance
|
|
18
14
|
// mutable state — every method is a pure function over the deps.
|
|
19
15
|
import fs from "node:fs";
|
|
20
|
-
import {
|
|
16
|
+
import { eq } from "drizzle-orm";
|
|
21
17
|
import git from "isomorphic-git";
|
|
22
|
-
import { pgErrorCode, PG_UNIQUE_VIOLATION
|
|
23
|
-
import {
|
|
18
|
+
import { pgErrorCode, PG_UNIQUE_VIOLATION } from "@intx/db";
|
|
19
|
+
import { asset as assetTable } from "@intx/db/schema";
|
|
24
20
|
import { generateId } from "@intx/hub-common";
|
|
25
21
|
import { getLogger } from "@intx/log";
|
|
26
22
|
const logger = getLogger(["hub-sessions", "asset-service"]);
|
|
@@ -30,14 +26,21 @@ const logger = getLogger(["hub-sessions", "asset-service"]);
|
|
|
30
26
|
* push to this ref so it carries the published-asset HEAD.
|
|
31
27
|
*/
|
|
32
28
|
export const DEFAULT_ASSET_REF = "refs/heads/main";
|
|
33
|
-
//
|
|
34
|
-
// session
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
29
|
+
// An asset name becomes a workspace mountpath segment for the asset
|
|
30
|
+
// kinds a session actually mounts: a tool-package registry mounts at
|
|
31
|
+
// `package-registries/<asset.name>/`. A skill is an asset kind too,
|
|
32
|
+
// and its `SKILL.md` frontmatter is validated on push and indexed
|
|
33
|
+
// under the pushed ref, but nothing consumes that index and no
|
|
34
|
+
// session path mounts a skill asset into an agent workspace. A skill
|
|
35
|
+
// asset is therefore recorded and validated, not materialized and not
|
|
36
|
+
// executed. The mountpath segment validator in applyAssetPack
|
|
37
|
+
// rejects anything outside a safe character set; validate at the
|
|
38
|
+
// createAsset boundary so a bad name fails at creation time rather
|
|
39
|
+
// than at materialization time. Names must be lowercase-kebab:
|
|
40
|
+
// lowercase letters, digits, hyphens, with no leading or trailing
|
|
41
|
+
// hyphen. The sole enforcement site is the createAsset check below,
|
|
42
|
+
// which rejects a bad name as `invalid_name`.
|
|
43
|
+
export const ASSET_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
41
44
|
export class AssetServiceError extends Error {
|
|
42
45
|
reason;
|
|
43
46
|
constructor(reason, message, cause) {
|
|
@@ -46,9 +49,6 @@ export class AssetServiceError extends Error {
|
|
|
46
49
|
this.reason = reason;
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
|
-
function isAccessMode(value) {
|
|
50
|
-
return value === "read-only" || value === "read-write";
|
|
51
|
-
}
|
|
52
52
|
/**
|
|
53
53
|
* Reject malformed `dir` arguments at the `listAssetBlobs` boundary
|
|
54
54
|
* before any tree walk begins. The empty string is the documented
|
|
@@ -112,19 +112,6 @@ function rowToAsset(row) {
|
|
|
112
112
|
updatedAt: row.updatedAt,
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
-
function rowToAgentAsset(row) {
|
|
116
|
-
if (!isAccessMode(row.accessMode)) {
|
|
117
|
-
throw new Error(`agent_asset row ${row.id} has unknown accessMode ${JSON.stringify(row.accessMode)}`);
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
id: row.id,
|
|
121
|
-
agentId: row.agentId,
|
|
122
|
-
assetId: row.assetId,
|
|
123
|
-
ref: row.ref,
|
|
124
|
-
accessMode: row.accessMode,
|
|
125
|
-
createdAt: row.createdAt,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
115
|
export function createAssetService(deps) {
|
|
129
116
|
const { db, repoStore } = deps;
|
|
130
117
|
const reservedPackageRegistryNames = deps.reservedPackageRegistryNames ?? new Set();
|
|
@@ -216,69 +203,6 @@ export function createAssetService(deps) {
|
|
|
216
203
|
throw err;
|
|
217
204
|
}
|
|
218
205
|
}
|
|
219
|
-
async function attachAsset(params) {
|
|
220
|
-
const id = generateId("agentAsset");
|
|
221
|
-
const accessMode = params.accessMode ?? "read-only";
|
|
222
|
-
const insertRow = {
|
|
223
|
-
id,
|
|
224
|
-
agentId: params.agentId,
|
|
225
|
-
assetId: params.assetId,
|
|
226
|
-
ref: params.ref,
|
|
227
|
-
accessMode,
|
|
228
|
-
createdAt: new Date(),
|
|
229
|
-
};
|
|
230
|
-
let inserted;
|
|
231
|
-
try {
|
|
232
|
-
const rows = await db
|
|
233
|
-
.insert(agentAssetTable)
|
|
234
|
-
.values(insertRow)
|
|
235
|
-
.returning();
|
|
236
|
-
const row = rows[0];
|
|
237
|
-
if (row === undefined) {
|
|
238
|
-
throw new Error("insert into agent_asset returned no rows");
|
|
239
|
-
}
|
|
240
|
-
inserted = row;
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
if (pgErrorCode(err) === PG_UNIQUE_VIOLATION) {
|
|
244
|
-
throw new AssetServiceError("duplicate_attachment", `agent_asset (agentId=${params.agentId}, assetId=${params.assetId}) already attached`, err);
|
|
245
|
-
}
|
|
246
|
-
if (pgErrorCode(err) === PG_FOREIGN_KEY_VIOLATION) {
|
|
247
|
-
throw new AssetServiceError("invalid_reference", `agent_asset (agentId=${params.agentId}, assetId=${params.assetId}) references a missing agent or asset`, err);
|
|
248
|
-
}
|
|
249
|
-
throw err;
|
|
250
|
-
}
|
|
251
|
-
return rowToAgentAsset(inserted);
|
|
252
|
-
}
|
|
253
|
-
async function listAgentAssets(agentId) {
|
|
254
|
-
// Order by (createdAt, id) so the row sequence is stable across reads
|
|
255
|
-
// — the available_skills stanza and pack fan-out both depend on a
|
|
256
|
-
// deterministic order, and Postgres does not guarantee one without
|
|
257
|
-
// an explicit orderBy.
|
|
258
|
-
const rows = await db
|
|
259
|
-
.select({
|
|
260
|
-
agentAsset: agentAssetTable,
|
|
261
|
-
asset: assetTable,
|
|
262
|
-
})
|
|
263
|
-
.from(agentAssetTable)
|
|
264
|
-
.innerJoin(assetTable, eq(agentAssetTable.assetId, assetTable.id))
|
|
265
|
-
.where(eq(agentAssetTable.agentId, agentId))
|
|
266
|
-
.orderBy(asc(agentAssetTable.createdAt), asc(agentAssetTable.id));
|
|
267
|
-
return rows.map((row) => {
|
|
268
|
-
const aa = rowToAgentAsset(row.agentAsset);
|
|
269
|
-
const a = rowToAsset(row.asset);
|
|
270
|
-
return {
|
|
271
|
-
...aa,
|
|
272
|
-
asset: {
|
|
273
|
-
id: a.id,
|
|
274
|
-
tenantId: a.tenantId,
|
|
275
|
-
kind: a.kind,
|
|
276
|
-
name: a.name,
|
|
277
|
-
displayName: a.displayName,
|
|
278
|
-
},
|
|
279
|
-
};
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
206
|
async function resolveAssetRowOrThrow(assetId, label) {
|
|
283
207
|
const row = await db.query.asset.findFirst({
|
|
284
208
|
where: eq(assetTable.id, assetId),
|
|
@@ -341,8 +265,6 @@ export function createAssetService(deps) {
|
|
|
341
265
|
return {
|
|
342
266
|
createAsset,
|
|
343
267
|
populateAsset,
|
|
344
|
-
attachAsset,
|
|
345
|
-
listAgentAssets,
|
|
346
268
|
readAssetBlob,
|
|
347
269
|
listAssetBlobs,
|
|
348
270
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommittedReads } from "./repo-store/types.js";
|
|
2
|
+
import type { SourceTreeReads } from "./workflow-source-closure.js";
|
|
3
|
+
/**
|
|
4
|
+
* Wrap `reads` so `readBlob(path)` resolves a repo-relative POSIX path to its
|
|
5
|
+
* blob: list the path's parent directory, match the final segment as a `blob`
|
|
6
|
+
* entry, then read by its object id. `listDir` and `treeOid` pass straight
|
|
7
|
+
* through -- both handles already speak repo-relative paths and return the same
|
|
8
|
+
* shapes.
|
|
9
|
+
*/
|
|
10
|
+
export declare function committedReadsToSourceTree(reads: CommittedReads): SourceTreeReads;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Adapts a repo-store `CommittedReads` handle (reads pinned to a commit,
|
|
2
|
+
// addressed by object id) to the `SourceTreeReads` shape the source-closure
|
|
3
|
+
// resolver consumes (reads addressed by repo-relative path). This bridge lives
|
|
4
|
+
// at the caller layer, not inside the resolver: the resolver stays agnostic of
|
|
5
|
+
// where its tree bytes come from (a test fake, this store, a future one), and
|
|
6
|
+
// the repo-store stays unaware of the closure interface. Only a layer that
|
|
7
|
+
// composes both types -- the install/deploy glue's caller -- owns the join.
|
|
8
|
+
/**
|
|
9
|
+
* Wrap `reads` so `readBlob(path)` resolves a repo-relative POSIX path to its
|
|
10
|
+
* blob: list the path's parent directory, match the final segment as a `blob`
|
|
11
|
+
* entry, then read by its object id. `listDir` and `treeOid` pass straight
|
|
12
|
+
* through -- both handles already speak repo-relative paths and return the same
|
|
13
|
+
* shapes.
|
|
14
|
+
*/
|
|
15
|
+
export function committedReadsToSourceTree(reads) {
|
|
16
|
+
return {
|
|
17
|
+
async readBlob(blobPath) {
|
|
18
|
+
// The parent of a top-level path (no slash) is the root tree, which
|
|
19
|
+
// `CommittedReads.listDir` addresses with the empty string.
|
|
20
|
+
const slash = blobPath.lastIndexOf("/");
|
|
21
|
+
const parentDir = slash === -1 ? "" : blobPath.slice(0, slash);
|
|
22
|
+
const name = slash === -1 ? blobPath : blobPath.slice(slash + 1);
|
|
23
|
+
const entries = await reads.listDir(parentDir);
|
|
24
|
+
// Match on the blob type so a path naming a directory or a gitlink fails
|
|
25
|
+
// loud here rather than handing a tree/commit oid to `readBlobByOid`.
|
|
26
|
+
const entry = entries.find((e) => e.name === name && e.type === "blob");
|
|
27
|
+
if (entry === undefined) {
|
|
28
|
+
throw new Error(`committedReadsToSourceTree: no blob at ${JSON.stringify(blobPath)}`);
|
|
29
|
+
}
|
|
30
|
+
return reads.readBlobByOid(entry.oid);
|
|
31
|
+
},
|
|
32
|
+
listDir: (dir) => reads.listDir(dir),
|
|
33
|
+
treeOid: (dir) => reads.treeOid(dir),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,32 +1,77 @@
|
|
|
1
1
|
import type { DB } from "@intx/db";
|
|
2
|
+
import type { CredentialCipher } from "@intx/types";
|
|
2
3
|
import type { SidecarRouter } from "./ws/sidecar-handler.js";
|
|
3
4
|
/**
|
|
4
5
|
* Re-resolve a single running instance's inference sources from the catalog
|
|
5
|
-
* (the
|
|
6
|
-
* the instance) and push the ordered list to its sidecar. The head of the
|
|
6
|
+
* (the definition's model requirements plus the invoker preferences persisted
|
|
7
|
+
* on the instance) and push the ordered list to its sidecar. The head of the
|
|
7
8
|
* catalog-priority-ordered list is the active default; the tail is the
|
|
8
9
|
* failover chain.
|
|
9
10
|
*
|
|
10
11
|
* No-op when the instance resolves to no launchable source — the resolver's
|
|
11
12
|
* own logger is the signal for why.
|
|
12
13
|
*/
|
|
13
|
-
export declare function pushInstanceSourceUpdate(db: DB["db"], sidecarRouter: Pick<SidecarRouter, "sendSourcesUpdate">, instance: {
|
|
14
|
+
export declare function pushInstanceSourceUpdate(db: DB["db"], sidecarRouter: Pick<SidecarRouter, "sendSourcesUpdate" | "sendCredentialsUpdate">, instance: {
|
|
14
15
|
address: string;
|
|
15
|
-
|
|
16
|
+
definitionId: string;
|
|
16
17
|
tenantId: string;
|
|
17
18
|
modelPreferences: unknown;
|
|
18
|
-
}): Promise<void>;
|
|
19
|
+
}, credentialCipher: CredentialCipher): Promise<void>;
|
|
19
20
|
/**
|
|
20
21
|
* After a credential secret is rotated, re-resolve every running instance in
|
|
21
22
|
* the tenant against the catalog and push the updates. A rotated secret flows
|
|
22
23
|
* through because resolution dereferences the provider's credential reference
|
|
23
24
|
* to the current secret.
|
|
24
25
|
*/
|
|
25
|
-
export declare function pushSourceUpdates(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string): Promise<void>;
|
|
26
|
+
export declare function pushSourceUpdates(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string, credentialCipher: CredentialCipher): Promise<void>;
|
|
26
27
|
/**
|
|
27
28
|
* After a catalog edit in a tenant, re-resolve and push to every running
|
|
28
29
|
* instance in that tenant AND its descendants. Descendants inherit the
|
|
29
30
|
* edited tenant's catalog, so a change there (a disabled provider, a new
|
|
30
31
|
* offering, a price update) alters their resolved sources too.
|
|
31
32
|
*/
|
|
32
|
-
export declare function pushSourceUpdatesSubtree(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string): Promise<void>;
|
|
33
|
+
export declare function pushSourceUpdatesSubtree(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string, credentialCipher: CredentialCipher): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* After a credential is deleted or deliberately revoked, evict it from every
|
|
36
|
+
* running deployment in the tenant AND its descendants. A descendant resolves
|
|
37
|
+
* an ancestor's tenant-owned credential through the tenant walk-up, so a
|
|
38
|
+
* revoke in one tenant can affect a descendant's run. The push is a flat named
|
|
39
|
+
* revocation: the child drops the credentialId's material and any binding that
|
|
40
|
+
* references it, and a run that never held it no-ops. Because a flat revoke is
|
|
41
|
+
* safe to broadcast, this needs no per-instance ledger of what was delivered.
|
|
42
|
+
*
|
|
43
|
+
* Callers fire this without awaiting, so it must never reject: a failure to
|
|
44
|
+
* enumerate or push is logged and dropped. This closes the ONLINE revocation
|
|
45
|
+
* window (a running deployment stops holding the credential now); the offline
|
|
46
|
+
* window -- a run whose sidecar was disconnected when the revoke fired -- is
|
|
47
|
+
* closed by the reconnect resync, not here.
|
|
48
|
+
*/
|
|
49
|
+
export declare function pushCredentialRevoke(db: DB["db"], sidecarRouter: Pick<SidecarRouter, "sendCredentialsUpdate">, tenantId: string, credentialId: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Reconcile a reconnecting deployment's credentials against its deploy-time set.
|
|
52
|
+
* Re-resolve the CURRENT material for every credentialId the deployment
|
|
53
|
+
* persisted at deploy (`workflow_run.credentialRefs`), then push a MERGE that
|
|
54
|
+
* upserts the survivors (picking up a same-id secret rotation) and REVOKES the
|
|
55
|
+
* deploy-time ids that no longer resolve (deleted or revoked while the sidecar
|
|
56
|
+
* was disconnected). Closes the OFFLINE revocation window (the online window is
|
|
57
|
+
* closed by `pushCredentialRevoke`).
|
|
58
|
+
*
|
|
59
|
+
* Merge, not wholesale-replace: `credentialRefs` is only the deploy-time id set,
|
|
60
|
+
* not the child's complete live set (a catalog re-point can deliver a new
|
|
61
|
+
* credential online), so a replace would evict online-added credentials. The
|
|
62
|
+
* merge upserts survivors and names the dead ids in `revoke`, leaving online
|
|
63
|
+
* credentials untouched. It does NOT handle an id-CHANGING rotation of a
|
|
64
|
+
* deploy-time source (the new id is not in `credentialRefs`); a later source
|
|
65
|
+
* push delivers that.
|
|
66
|
+
*
|
|
67
|
+
* No-op when the run persisted no credential refs (a folded run, or a
|
|
68
|
+
* deployment with no credentials). Fire-and-forget from the reconnect handler:
|
|
69
|
+
* it never rejects. A live-but-unresolvable credential (its provider vanished
|
|
70
|
+
* or has no API base URL) makes `reresolveCurrentMaterials` throw, which aborts
|
|
71
|
+
* the WHOLE reconcile (logged, not sent) so a partial set with a spurious
|
|
72
|
+
* revoke never lands. This is deliberately all-or-nothing: one misconfigured
|
|
73
|
+
* credential blocks this reconnect's revocation of the others too, trading
|
|
74
|
+
* revocation timeliness for never falsely evicting a live credential. The next
|
|
75
|
+
* reconnect (or an online revoke) retries.
|
|
76
|
+
*/
|
|
77
|
+
export declare function pushCredentialReconcile(db: DB["db"], sidecarRouter: Pick<SidecarRouter, "sendCredentialsUpdate">, agentAddress: string, credentialCipher: CredentialCipher): Promise<void>;
|