@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.
Files changed (87) hide show
  1. package/README.md +3 -5
  2. package/dist/agent-repo.d.ts +23 -7
  3. package/dist/agent-repo.js +19 -6
  4. package/dist/agent-state-kind.js +18 -63
  5. package/dist/asset-service.d.ts +1 -20
  6. package/dist/asset-service.js +19 -97
  7. package/dist/committed-source-tree.d.ts +10 -0
  8. package/dist/committed-source-tree.js +35 -0
  9. package/dist/credential-push.d.ts +52 -7
  10. package/dist/credential-push.js +178 -22
  11. package/dist/event-collector-registry.d.ts +3 -2
  12. package/dist/event-collector-registry.js +42 -13
  13. package/dist/event-collector.d.ts +12 -2
  14. package/dist/event-collector.js +45 -4
  15. package/dist/hub-session-lookups.d.ts +125 -7
  16. package/dist/hub-session-lookups.js +541 -86
  17. package/dist/hub-session-orchestrator.d.ts +2 -3
  18. package/dist/hub-session-orchestrator.js +22 -56
  19. package/dist/index.d.ts +19 -9
  20. package/dist/index.js +16 -7
  21. package/dist/reconciliation-scheduler.d.ts +14 -0
  22. package/dist/reconciliation-scheduler.js +55 -0
  23. package/dist/repo-store/index.d.ts +2 -1
  24. package/dist/repo-store/index.js +1 -0
  25. package/dist/repo-store/store.d.ts +1 -1
  26. package/dist/repo-store/store.js +138 -1
  27. package/dist/repo-store/subscribe-kind.d.ts +6 -3
  28. package/dist/repo-store/subscribe-kind.js +42 -77
  29. package/dist/repo-store/types.d.ts +94 -6
  30. package/dist/repo-store/user-principal-gate.d.ts +26 -0
  31. package/dist/repo-store/user-principal-gate.js +78 -0
  32. package/dist/session-service.d.ts +252 -126
  33. package/dist/session-service.js +851 -624
  34. package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
  35. package/dist/sidecar-allocation/capability-policy.js +124 -0
  36. package/dist/sidecar-allocation/contracts.d.ts +101 -0
  37. package/dist/sidecar-allocation/contracts.js +26 -0
  38. package/dist/sidecar-allocation/index.d.ts +5 -0
  39. package/dist/sidecar-allocation/index.js +4 -0
  40. package/dist/sidecar-allocation/operation.d.ts +10 -0
  41. package/dist/sidecar-allocation/operation.js +54 -0
  42. package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
  43. package/dist/sidecar-allocation/plugin-registry.js +61 -0
  44. package/dist/sidecar-allocation/reconciler.d.ts +54 -0
  45. package/dist/sidecar-allocation/reconciler.js +825 -0
  46. package/dist/skill-kind.js +12 -62
  47. package/dist/substrate.d.ts +3 -3
  48. package/dist/substrate.js +1 -1
  49. package/dist/workflow-allocation-service.d.ts +64 -0
  50. package/dist/workflow-allocation-service.js +554 -0
  51. package/dist/workflow-closure-resolution.d.ts +106 -0
  52. package/dist/workflow-closure-resolution.js +123 -0
  53. package/dist/workflow-definition-ensure.d.ts +24 -0
  54. package/dist/workflow-definition-ensure.js +75 -0
  55. package/dist/workflow-dispatch-service.d.ts +42 -0
  56. package/dist/workflow-dispatch-service.js +209 -0
  57. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  58. package/dist/workflow-dispatch-settlement.js +140 -0
  59. package/dist/workflow-kind.d.ts +29 -1
  60. package/dist/workflow-kind.js +143 -139
  61. package/dist/workflow-probe-gate.d.ts +286 -0
  62. package/dist/workflow-probe-gate.js +382 -0
  63. package/dist/workflow-run-kind.d.ts +239 -32
  64. package/dist/workflow-run-kind.js +874 -188
  65. package/dist/workflow-run-reader.d.ts +1 -1
  66. package/dist/workflow-run-reader.js +3 -7
  67. package/dist/workflow-run-restore.d.ts +16 -0
  68. package/dist/workflow-run-restore.js +30 -0
  69. package/dist/workflow-source-closure.d.ts +35 -0
  70. package/dist/workflow-source-closure.js +342 -0
  71. package/dist/workflow-source-pins.d.ts +8 -0
  72. package/dist/workflow-source-pins.js +14 -0
  73. package/dist/ws/index.d.ts +3 -3
  74. package/dist/ws/index.js +2 -2
  75. package/dist/ws/pending-tracker.d.ts +93 -0
  76. package/dist/ws/pending-tracker.js +132 -0
  77. package/dist/ws/sidecar-events.d.ts +139 -37
  78. package/dist/ws/sidecar-events.js +2 -2
  79. package/dist/ws/sidecar-handler.d.ts +218 -60
  80. package/dist/ws/sidecar-handler.js +1713 -732
  81. package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
  82. package/dist/ws/sidecar-handler.test-helpers.js +95 -0
  83. package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
  84. package/dist/ws/sidecar-token-authenticator.js +78 -7
  85. package/package.json +14 -13
  86. package/dist/available-skills-stanza.d.ts +0 -21
  87. 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`, `AgentAsset`,
99
- `AgentAssetWithAsset`, `AccessMode`, `CreateAssetParams`,
100
- `PopulateAssetParams`, `AttachAssetParams`,
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
 
@@ -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 agent address internally. Stamps an
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 `deploymentId` equals
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<void>;
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: {
@@ -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 ? {} : { gc: { kinds: ["agent-state"], ...gc } }),
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
- deploymentId: incomingRepoId.id,
112
+ anchorRunId: incomingRepoId.id,
97
113
  };
98
114
  const expectedOldSha = await store.resolveRef(hub, incomingRepoId, ref);
99
- await store.receivePack(principal, incomingRepoId, ref, pack, commitSha, expectedOldSha);
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;
@@ -1,6 +1,5 @@
1
1
  import { type } from "arktype";
2
- import { glob, repoActionToGrantVerb } from "@intx/hub-common";
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 (getDeployRef,
66
- // createDeployPack) and writes (writeDeployTree) all flow through
67
- // here, so changing this branch tightens behavior in non-obvious
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
- // The route layer has already pre-resolved the grant verdict and
118
- // attached it as `authz`. The substrate does NOT re-query the
119
- // grant store here; it (a) checks the bearer-token's claims
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
- if (!parsed.tokenClaims.actions.includes(action)) {
138
- return {
139
- allowed: false,
140
- reason: `token does not grant action ${action}`,
141
- };
142
- }
143
- // `ref === "*"` is the substrate's sentinel for the bulk read
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}`,
@@ -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);
@@ -1,4 +1,4 @@
1
- // In-process service for creating and attaching skill-asset repos.
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 { asc, eq } from "drizzle-orm";
16
+ import { eq } from "drizzle-orm";
21
17
  import git from "isomorphic-git";
22
- import { pgErrorCode, PG_UNIQUE_VIOLATION, PG_FOREIGN_KEY_VIOLATION, } from "@intx/db";
23
- import { agentAsset as agentAssetTable, asset as assetTable, } from "@intx/db/schema";
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
- // Asset names become the default workspace mountpath segment at
34
- // session start (`skills/<asset.name>/`). The mountpath segment
35
- // validator in applyAssetPack rejects anything outside a safe
36
- // character set; validate at the createAsset boundary so a bad name
37
- // fails at creation time rather than at materialization time. Names
38
- // must be lowercase-kebab: lowercase letters, digits, hyphens, with
39
- // no leading or trailing hyphen.
40
- const ASSET_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
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 agent's model requirements plus the invoker preferences persisted on
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
- agentId: string;
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>;