@intx/hub-sessions 0.1.2 → 0.3.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/LICENSE +176 -0
- package/README.md +82 -1
- package/dist/agent-repo.d.ts +93 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +189 -0
- package/dist/asset-service.d.ts +104 -0
- package/dist/asset-service.js +267 -0
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +33 -0
- package/dist/credential-push.js +109 -0
- package/dist/event-collector-registry.d.ts +20 -0
- package/dist/event-collector-registry.js +115 -0
- package/dist/event-collector.d.ts +39 -0
- package/dist/event-collector.js +365 -0
- package/dist/hub-session-lookups.d.ts +135 -0
- package/dist/hub-session-lookups.js +663 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +87 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/package-registry-kind.d.ts +70 -0
- package/dist/package-registry-kind.js +260 -0
- package/dist/repo-store/index.d.ts +4 -0
- package/dist/repo-store/index.js +3 -0
- package/dist/repo-store/store.d.ts +41 -0
- package/dist/repo-store/store.js +1829 -0
- package/dist/repo-store/subscribe-kind.d.ts +56 -0
- package/dist/repo-store/subscribe-kind.js +144 -0
- package/dist/repo-store/types.d.ts +571 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +416 -0
- package/dist/session-service.js +1191 -0
- package/dist/sidecar-allocation/contracts.d.ts +78 -0
- package/dist/sidecar-allocation/contracts.js +21 -0
- package/dist/sidecar-allocation/index.d.ts +4 -0
- package/dist/sidecar-allocation/index.js +3 -0
- package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
- package/dist/sidecar-allocation/placement-policy.js +21 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
- package/dist/sidecar-allocation/plugin-registry.js +37 -0
- package/dist/sidecar-allocation/reconciler.d.ts +42 -0
- package/dist/sidecar-allocation/reconciler.js +431 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +292 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-allocation-service.d.ts +58 -0
- package/dist/workflow-allocation-service.js +239 -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 +40 -0
- package/dist/workflow-dispatch-service.js +146 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +37 -0
- package/dist/workflow-kind.js +310 -0
- package/dist/workflow-probe-gate.d.ts +214 -0
- package/dist/workflow-probe-gate.js +207 -0
- package/dist/workflow-run-event-log.d.ts +21 -0
- package/dist/workflow-run-event-log.js +51 -0
- package/dist/workflow-run-kind.d.ts +440 -0
- package/dist/workflow-run-kind.js +2916 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +153 -0
- package/dist/workflow-run-restore.d.ts +15 -0
- package/dist/workflow-run-restore.js +26 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +222 -0
- package/dist/ws/sidecar-events.js +72 -0
- package/dist/ws/sidecar-handler.d.ts +305 -0
- package/dist/ws/sidecar-handler.js +2537 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
- package/dist/ws/sidecar-token-authenticator.js +81 -0
- package/package.json +35 -12
- package/src/agent-repo.test.ts +0 -310
- package/src/agent-repo.ts +0 -165
- package/src/agent-state-kind.test.ts +0 -247
- package/src/agent-state-kind.ts +0 -204
- package/src/asset-service.test.ts +0 -540
- package/src/asset-service.ts +0 -378
- package/src/available-skills-stanza.test.ts +0 -87
- package/src/available-skills-stanza.ts +0 -47
- package/src/credential-push.ts +0 -65
- package/src/event-collector-registry.test.ts +0 -73
- package/src/event-collector-registry.ts +0 -171
- package/src/event-collector.test.ts +0 -1387
- package/src/event-collector.ts +0 -424
- package/src/hub-session-lookups.ts +0 -206
- package/src/hub-session-orchestrator.test.ts +0 -510
- package/src/hub-session-orchestrator.ts +0 -213
- package/src/index.ts +0 -78
- package/src/repo-store/index.ts +0 -15
- package/src/repo-store/store.test.ts +0 -1169
- package/src/repo-store/store.ts +0 -428
- package/src/repo-store/types.ts +0 -253
- package/src/session-service.test.ts +0 -895
- package/src/session-service.ts +0 -464
- package/src/skill-kind.test.ts +0 -599
- package/src/skill-kind.ts +0 -350
- package/src/ws/index.ts +0 -18
- package/src/ws/sidecar-events.test.ts +0 -96
- package/src/ws/sidecar-events.ts +0 -231
- package/src/ws/sidecar-handler.test.ts +0 -2217
- package/src/ws/sidecar-handler.ts +0 -1574
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DB } from "@intx/db";
|
|
2
|
+
import type { SidecarCredentialResolver } from "../sidecar-allocation/contracts.js";
|
|
3
|
+
import type { SidecarAuthenticator } from "./sidecar-handler.js";
|
|
4
|
+
export type CreateSidecarTokenAuthenticatorDeps = {
|
|
5
|
+
db: DB["db"];
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Builds an authenticator that verifies a sidecar's presented token
|
|
9
|
+
* against the per-sidecar hash stored on the `sidecar` table. The token
|
|
10
|
+
* is hashed with SHA-256 and looked up by its digest; a matching row
|
|
11
|
+
* yields that row's id as the verified identity, and an unknown token
|
|
12
|
+
* resolves to `null` so the handshake is rejected. The claimed
|
|
13
|
+
* `sidecarId` on the frame is ignored: identity is derived from the
|
|
14
|
+
* token alone.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createSidecarCredentialResolver({ db, }: CreateSidecarTokenAuthenticatorDeps): SidecarCredentialResolver;
|
|
17
|
+
export declare function createSidecarTokenAuthenticator(deps: CreateSidecarTokenAuthenticatorDeps): SidecarAuthenticator;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
2
|
+
import { sha256 } from "@intx/crypto";
|
|
3
|
+
import { sidecar, sidecarAllocation, workflowRun } from "@intx/db/schema";
|
|
4
|
+
/**
|
|
5
|
+
* Builds an authenticator that verifies a sidecar's presented token
|
|
6
|
+
* against the per-sidecar hash stored on the `sidecar` table. The token
|
|
7
|
+
* is hashed with SHA-256 and looked up by its digest; a matching row
|
|
8
|
+
* yields that row's id as the verified identity, and an unknown token
|
|
9
|
+
* resolves to `null` so the handshake is rejected. The claimed
|
|
10
|
+
* `sidecarId` on the frame is ignored: identity is derived from the
|
|
11
|
+
* token alone.
|
|
12
|
+
*/
|
|
13
|
+
export function createSidecarCredentialResolver({ db, }) {
|
|
14
|
+
async function resolve(token) {
|
|
15
|
+
const tokenHash = await sha256(token);
|
|
16
|
+
const row = await db.query.sidecar.findFirst({
|
|
17
|
+
where: eq(sidecar.tokenHashSha256, tokenHash),
|
|
18
|
+
});
|
|
19
|
+
if (row === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
if (row.credentialScope === "shared") {
|
|
22
|
+
return { kind: "shared", sidecarId: row.id };
|
|
23
|
+
}
|
|
24
|
+
const allocation = await db.query.sidecarAllocation.findFirst({
|
|
25
|
+
where: eq(sidecarAllocation.sidecarId, row.id),
|
|
26
|
+
});
|
|
27
|
+
if (allocation === undefined)
|
|
28
|
+
return null;
|
|
29
|
+
if (allocation.status !== "provisioning" &&
|
|
30
|
+
allocation.status !== "allocated") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const anchor = await db.query.workflowRun.findFirst({
|
|
34
|
+
columns: { address: true },
|
|
35
|
+
where: eq(workflowRun.id, allocation.anchorRunId),
|
|
36
|
+
});
|
|
37
|
+
if (anchor?.address === null || anchor?.address === undefined)
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
kind: "allocated",
|
|
41
|
+
sidecarId: row.id,
|
|
42
|
+
allocationId: allocation.id,
|
|
43
|
+
tenantId: allocation.tenantId,
|
|
44
|
+
anchorRunId: allocation.anchorRunId,
|
|
45
|
+
workflowRunAddress: anchor.address,
|
|
46
|
+
generation: allocation.generation,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function isCurrent(identity, use) {
|
|
50
|
+
if (identity.kind === "shared") {
|
|
51
|
+
const allocation = await db.query.sidecarAllocation.findFirst({
|
|
52
|
+
columns: { id: true },
|
|
53
|
+
where: eq(sidecarAllocation.sidecarId, identity.sidecarId),
|
|
54
|
+
});
|
|
55
|
+
return allocation === undefined;
|
|
56
|
+
}
|
|
57
|
+
const statuses = use === "registration"
|
|
58
|
+
? ["provisioning", "allocated"]
|
|
59
|
+
: ["allocated"];
|
|
60
|
+
const allocation = await db.query.sidecarAllocation.findFirst({
|
|
61
|
+
where: and(eq(sidecarAllocation.id, identity.allocationId), eq(sidecarAllocation.sidecarId, identity.sidecarId), eq(sidecarAllocation.tenantId, identity.tenantId), eq(sidecarAllocation.anchorRunId, identity.anchorRunId), eq(sidecarAllocation.generation, identity.generation), inArray(sidecarAllocation.status, statuses)),
|
|
62
|
+
});
|
|
63
|
+
if (allocation === undefined)
|
|
64
|
+
return false;
|
|
65
|
+
if (use === "registration")
|
|
66
|
+
return true;
|
|
67
|
+
if (allocation.ensureAcceptedGeneration !== identity.generation) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const anchor = await db.query.workflowRun.findFirst({
|
|
71
|
+
columns: { address: true },
|
|
72
|
+
where: eq(workflowRun.id, identity.anchorRunId),
|
|
73
|
+
});
|
|
74
|
+
return anchor?.address === identity.workflowRunAddress;
|
|
75
|
+
}
|
|
76
|
+
return { resolve, isCurrent };
|
|
77
|
+
}
|
|
78
|
+
export function createSidecarTokenAuthenticator(deps) {
|
|
79
|
+
const resolver = createSidecarCredentialResolver(deps);
|
|
80
|
+
return async ({ token }) => resolver.resolve(token);
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intx/hub-sessions",
|
|
3
|
-
"
|
|
3
|
+
"description": "Session-orchestration substrate for the hub: sidecar router, session, and asset services",
|
|
4
|
+
"version": "0.3.0",
|
|
4
5
|
"license": "LGPL-2.1-only",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
9
|
+
"intx-src": "./src/index.ts",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./substrate": {
|
|
14
|
+
"intx-src": "./src/substrate.ts",
|
|
15
|
+
"types": "./dist/substrate.d.ts",
|
|
16
|
+
"default": "./dist/substrate.js"
|
|
10
17
|
}
|
|
11
18
|
},
|
|
12
19
|
"dependencies": {
|
|
13
|
-
"@intx/
|
|
14
|
-
"@intx/
|
|
15
|
-
"@intx/
|
|
16
|
-
"@intx/
|
|
17
|
-
"@intx/
|
|
18
|
-
"@intx/
|
|
19
|
-
"@intx/
|
|
20
|
-
"@intx/
|
|
21
|
-
"
|
|
20
|
+
"@intx/agent": "0.3.0",
|
|
21
|
+
"@intx/crypto": "0.3.0",
|
|
22
|
+
"@intx/db": "0.3.0",
|
|
23
|
+
"@intx/hub-common": "0.3.0",
|
|
24
|
+
"@intx/log": "0.3.0",
|
|
25
|
+
"@intx/mime": "0.3.0",
|
|
26
|
+
"@intx/pack-transport": "0.3.0",
|
|
27
|
+
"@intx/storage-isogit": "0.3.0",
|
|
28
|
+
"@intx/tool-packaging": "0.3.0",
|
|
29
|
+
"@intx/types": "0.3.0",
|
|
30
|
+
"@intx/workflow": "0.3.0",
|
|
31
|
+
"@intx/workflow-deploy": "0.3.0",
|
|
32
|
+
"arktype": "^2.1.29",
|
|
33
|
+
"drizzle-orm": "^0.45.1",
|
|
34
|
+
"isomorphic-git": "^1.27.2",
|
|
35
|
+
"tar": "^7.5.1"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
22
45
|
}
|
|
23
46
|
}
|
package/src/agent-repo.test.ts
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, afterAll, beforeAll } from "bun:test";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import git from "isomorphic-git";
|
|
6
|
-
import { generateKeyPair } from "@intx/crypto-node";
|
|
7
|
-
import { createDeployPack } from "@intx/storage-isogit";
|
|
8
|
-
import { createAgentRepoStore } from "./agent-repo";
|
|
9
|
-
import type { KeyPair } from "@intx/types/runtime";
|
|
10
|
-
|
|
11
|
-
const tempDirs: string[] = [];
|
|
12
|
-
|
|
13
|
-
async function makeTempDir(prefix: string): Promise<string> {
|
|
14
|
-
const d = await fs.promises.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
15
|
-
tempDirs.push(d);
|
|
16
|
-
return d;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let signingKey: KeyPair;
|
|
20
|
-
|
|
21
|
-
beforeAll(async () => {
|
|
22
|
-
signingKey = await generateKeyPair();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterAll(async () => {
|
|
26
|
-
for (const d of tempDirs.splice(0)) {
|
|
27
|
-
await fs.promises.rm(d, { recursive: true, force: true }).catch((_e) => {
|
|
28
|
-
/* best effort cleanup */
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe("AgentRepoStore", () => {
|
|
34
|
-
test("writeDeployTree creates a commit with the system prompt", async () => {
|
|
35
|
-
const dataDir = await makeTempDir("agent-repo-");
|
|
36
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
37
|
-
|
|
38
|
-
const { commitSha } = await store.writeDeployTree("agent-1", {
|
|
39
|
-
systemPrompt: "You are a test agent.",
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
expect(commitSha).toMatch(/^[0-9a-f]{40}$/);
|
|
43
|
-
|
|
44
|
-
const repoDir = path.join(dataDir, "agents", "agent-1");
|
|
45
|
-
const prompt = await fs.promises.readFile(
|
|
46
|
-
path.join(repoDir, "deploy", "prompt.md"),
|
|
47
|
-
"utf-8",
|
|
48
|
-
);
|
|
49
|
-
expect(prompt).toBe("You are a test agent.");
|
|
50
|
-
|
|
51
|
-
const ref = await git.resolveRef({
|
|
52
|
-
fs,
|
|
53
|
-
dir: repoDir,
|
|
54
|
-
ref: "refs/heads/deploy",
|
|
55
|
-
});
|
|
56
|
-
expect(ref).toBe(commitSha);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("writeDeployTree does not advance refs/heads/main", async () => {
|
|
60
|
-
const dataDir = await makeTempDir("agent-repo-ref-");
|
|
61
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
62
|
-
|
|
63
|
-
await store.writeDeployTree("agent-ref", {
|
|
64
|
-
systemPrompt: "Ref test.",
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const repoDir = path.join(dataDir, "agents", "agent-ref");
|
|
68
|
-
const mainLog = await git.log({ fs, dir: repoDir, ref: "refs/heads/main" });
|
|
69
|
-
const deployLog = await git.log({
|
|
70
|
-
fs,
|
|
71
|
-
dir: repoDir,
|
|
72
|
-
ref: "refs/heads/deploy",
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// main should only have the init commit
|
|
76
|
-
expect(mainLog.length).toBe(1);
|
|
77
|
-
// deploy should have 2: init parent + deploy commit
|
|
78
|
-
expect(deployLog.length).toBe(2);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("createDeployPack produces a valid packfile", async () => {
|
|
82
|
-
const dataDir = await makeTempDir("agent-repo-pack-");
|
|
83
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
84
|
-
|
|
85
|
-
await store.writeDeployTree("agent-2", {
|
|
86
|
-
systemPrompt: "Pack test.",
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const { pack, commitSha, ref } = await store.createDeployPack("agent-2");
|
|
90
|
-
|
|
91
|
-
expect(pack).toBeInstanceOf(Uint8Array);
|
|
92
|
-
expect(pack.length).toBeGreaterThan(0);
|
|
93
|
-
expect(commitSha).toMatch(/^[0-9a-f]{40}$/);
|
|
94
|
-
expect(ref).toBe("refs/heads/deploy");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("receiveStatePack indexes objects and updates ref", async () => {
|
|
98
|
-
const dataDir = await makeTempDir("agent-repo-state-");
|
|
99
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
100
|
-
|
|
101
|
-
await store.writeDeployTree("agent-3", {
|
|
102
|
-
systemPrompt: "State test.",
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const sourceDir = await makeTempDir("state-source-");
|
|
106
|
-
await git.init({ fs, dir: sourceDir, defaultBranch: "main" });
|
|
107
|
-
await fs.promises.mkdir(path.join(sourceDir, "state"), { recursive: true });
|
|
108
|
-
await fs.promises.writeFile(
|
|
109
|
-
path.join(sourceDir, "state", "turns.jsonl"),
|
|
110
|
-
'{"messages":[]}',
|
|
111
|
-
);
|
|
112
|
-
await git.add({ fs, dir: sourceDir, filepath: "state/turns.jsonl" });
|
|
113
|
-
const stateCommit = await git.commit({
|
|
114
|
-
fs,
|
|
115
|
-
dir: sourceDir,
|
|
116
|
-
message: "State snapshot",
|
|
117
|
-
author: { name: "test", email: "test@test" },
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const { pack } = await createDeployPack(sourceDir, "refs/heads/main");
|
|
121
|
-
|
|
122
|
-
const stateRef = "refs/instances/test-instance";
|
|
123
|
-
await store.receiveStatePack(
|
|
124
|
-
{ kind: "agent-state", id: "agent-3" },
|
|
125
|
-
pack,
|
|
126
|
-
stateRef,
|
|
127
|
-
stateCommit,
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const repoDir = path.join(dataDir, "agents", "agent-3");
|
|
131
|
-
const resolved = await git.resolveRef({
|
|
132
|
-
fs,
|
|
133
|
-
dir: repoDir,
|
|
134
|
-
ref: stateRef,
|
|
135
|
-
});
|
|
136
|
-
expect(resolved).toBe(stateCommit);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("receiveStatePack accepts packs with .gitignore alongside state", async () => {
|
|
140
|
-
const dataDir = await makeTempDir("agent-repo-gitignore-");
|
|
141
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
142
|
-
|
|
143
|
-
await store.writeDeployTree("agent-gi", {
|
|
144
|
-
systemPrompt: "Gitignore test.",
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const sourceDir = await makeTempDir("gitignore-source-");
|
|
148
|
-
await git.init({ fs, dir: sourceDir, defaultBranch: "main" });
|
|
149
|
-
await fs.promises.writeFile(path.join(sourceDir, ".gitignore"), "keys/\n");
|
|
150
|
-
await fs.promises.mkdir(path.join(sourceDir, "state"), { recursive: true });
|
|
151
|
-
await fs.promises.writeFile(
|
|
152
|
-
path.join(sourceDir, "state", "turns.jsonl"),
|
|
153
|
-
"{}",
|
|
154
|
-
);
|
|
155
|
-
await git.add({ fs, dir: sourceDir, filepath: ".gitignore" });
|
|
156
|
-
await git.add({ fs, dir: sourceDir, filepath: "state/turns.jsonl" });
|
|
157
|
-
const stateCommit = await git.commit({
|
|
158
|
-
fs,
|
|
159
|
-
dir: sourceDir,
|
|
160
|
-
message: "State with gitignore",
|
|
161
|
-
author: { name: "test", email: "test@test" },
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const { pack } = await createDeployPack(sourceDir, "refs/heads/main");
|
|
165
|
-
const stateRef = "refs/instances/gi-test";
|
|
166
|
-
await store.receiveStatePack(
|
|
167
|
-
{ kind: "agent-state", id: "agent-gi" },
|
|
168
|
-
pack,
|
|
169
|
-
stateRef,
|
|
170
|
-
stateCommit,
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const repoDir = path.join(dataDir, "agents", "agent-gi");
|
|
174
|
-
const resolved = await git.resolveRef({
|
|
175
|
-
fs,
|
|
176
|
-
dir: repoDir,
|
|
177
|
-
ref: stateRef,
|
|
178
|
-
});
|
|
179
|
-
expect(resolved).toBe(stateCommit);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test("receiveStatePack rejects packs with only .gitignore and no state/", async () => {
|
|
183
|
-
const dataDir = await makeTempDir("agent-repo-gitignore-only-");
|
|
184
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
185
|
-
|
|
186
|
-
await store.writeDeployTree("agent-gio", {
|
|
187
|
-
systemPrompt: "Gitignore-only test.",
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const sourceDir = await makeTempDir("gitignore-only-source-");
|
|
191
|
-
await git.init({ fs, dir: sourceDir, defaultBranch: "main" });
|
|
192
|
-
await fs.promises.writeFile(path.join(sourceDir, ".gitignore"), "keys/\n");
|
|
193
|
-
await git.add({ fs, dir: sourceDir, filepath: ".gitignore" });
|
|
194
|
-
const badCommit = await git.commit({
|
|
195
|
-
fs,
|
|
196
|
-
dir: sourceDir,
|
|
197
|
-
message: "Only gitignore",
|
|
198
|
-
author: { name: "test", email: "test@test" },
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const { pack } = await createDeployPack(sourceDir, "refs/heads/main");
|
|
202
|
-
|
|
203
|
-
await expect(
|
|
204
|
-
store.receiveStatePack(
|
|
205
|
-
{ kind: "agent-state", id: "agent-gio" },
|
|
206
|
-
pack,
|
|
207
|
-
"refs/instances/test",
|
|
208
|
-
badCommit,
|
|
209
|
-
),
|
|
210
|
-
).rejects.toThrow("path_violation");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("receiveStatePack rejects packs with paths outside state/", async () => {
|
|
214
|
-
const dataDir = await makeTempDir("agent-repo-confined-");
|
|
215
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
216
|
-
|
|
217
|
-
await store.writeDeployTree("agent-confined", {
|
|
218
|
-
systemPrompt: "Confinement test.",
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const sourceDir = await makeTempDir("confined-source-");
|
|
222
|
-
await git.init({ fs, dir: sourceDir, defaultBranch: "main" });
|
|
223
|
-
await fs.promises.mkdir(path.join(sourceDir, "state"), { recursive: true });
|
|
224
|
-
await fs.promises.mkdir(path.join(sourceDir, "deploy"), {
|
|
225
|
-
recursive: true,
|
|
226
|
-
});
|
|
227
|
-
await fs.promises.writeFile(
|
|
228
|
-
path.join(sourceDir, "state", "turns.jsonl"),
|
|
229
|
-
"{}",
|
|
230
|
-
);
|
|
231
|
-
await fs.promises.writeFile(
|
|
232
|
-
path.join(sourceDir, "deploy", "prompt.md"),
|
|
233
|
-
"evil",
|
|
234
|
-
);
|
|
235
|
-
await git.add({ fs, dir: sourceDir, filepath: "state/turns.jsonl" });
|
|
236
|
-
await git.add({ fs, dir: sourceDir, filepath: "deploy/prompt.md" });
|
|
237
|
-
const badCommit = await git.commit({
|
|
238
|
-
fs,
|
|
239
|
-
dir: sourceDir,
|
|
240
|
-
message: "Escaped confinement",
|
|
241
|
-
author: { name: "test", email: "test@test" },
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const { pack } = await createDeployPack(sourceDir, "refs/heads/main");
|
|
245
|
-
|
|
246
|
-
await expect(
|
|
247
|
-
store.receiveStatePack(
|
|
248
|
-
{ kind: "agent-state", id: "agent-confined" },
|
|
249
|
-
pack,
|
|
250
|
-
"refs/instances/test",
|
|
251
|
-
badCommit,
|
|
252
|
-
),
|
|
253
|
-
).rejects.toThrow("path_violation");
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test("writeDeployTree produces a fresh commit when content changes", async () => {
|
|
257
|
-
const dataDir = await makeTempDir("agent-repo-idem-");
|
|
258
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
259
|
-
|
|
260
|
-
const first = await store.writeDeployTree("agent-4", {
|
|
261
|
-
systemPrompt: "Version 1",
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
const second = await store.writeDeployTree("agent-4", {
|
|
265
|
-
systemPrompt: "Version 2",
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
expect(second.commitSha).not.toBe(first.commitSha);
|
|
269
|
-
|
|
270
|
-
const repoDir = path.join(dataDir, "agents", "agent-4");
|
|
271
|
-
const prompt = await fs.promises.readFile(
|
|
272
|
-
path.join(repoDir, "deploy", "prompt.md"),
|
|
273
|
-
"utf-8",
|
|
274
|
-
);
|
|
275
|
-
expect(prompt).toBe("Version 2");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("hub repo does not contain state/ scaffolding", async () => {
|
|
279
|
-
const dataDir = await makeTempDir("agent-repo-nostate-");
|
|
280
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
281
|
-
|
|
282
|
-
await store.writeDeployTree("agent-5", {
|
|
283
|
-
systemPrompt: "No state test.",
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const repoDir = path.join(dataDir, "agents", "agent-5");
|
|
287
|
-
const stateExists = await fs.promises
|
|
288
|
-
.stat(path.join(repoDir, "state"))
|
|
289
|
-
.then(() => true)
|
|
290
|
-
.catch(() => false);
|
|
291
|
-
expect(stateExists).toBe(false);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("rejects agent IDs with path traversal characters", () => {
|
|
295
|
-
const dataDir = "/tmp/never-created";
|
|
296
|
-
const store = createAgentRepoStore({ dataDir, signingKey });
|
|
297
|
-
|
|
298
|
-
expect(() =>
|
|
299
|
-
store.writeDeployTree("../../evil", {
|
|
300
|
-
systemPrompt: "x",
|
|
301
|
-
}),
|
|
302
|
-
).toThrow("repo_id_invalid: ../../evil");
|
|
303
|
-
|
|
304
|
-
expect(() =>
|
|
305
|
-
store.writeDeployTree("agent@domain", {
|
|
306
|
-
systemPrompt: "x",
|
|
307
|
-
}),
|
|
308
|
-
).toThrow("repo_id_invalid: agent@domain");
|
|
309
|
-
});
|
|
310
|
-
});
|
package/src/agent-repo.ts
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { createSSHSignature } from "@intx/crypto-node";
|
|
2
|
-
|
|
3
|
-
import { createRepoStore } from "./repo-store";
|
|
4
|
-
import type { AuthorizeFn, RepoId, RepoStore } from "./repo-store";
|
|
5
|
-
import {
|
|
6
|
-
agentStateKindHandler,
|
|
7
|
-
agentStateAuthorize,
|
|
8
|
-
AGENT_STATE_DEPLOY_REF,
|
|
9
|
-
type AgentStateHubPrincipal,
|
|
10
|
-
type AgentStateSidecarPrincipal,
|
|
11
|
-
} from "./agent-state-kind";
|
|
12
|
-
import { skillKindHandler, skillAuthorize } from "./skill-kind";
|
|
13
|
-
|
|
14
|
-
export type DeployContent = {
|
|
15
|
-
systemPrompt: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type AgentRepoStore = {
|
|
19
|
-
/**
|
|
20
|
-
* Write deploy content into the agent's hub-side repo and commit on
|
|
21
|
-
* refs/heads/deploy. Creates the repo if it doesn't exist.
|
|
22
|
-
*
|
|
23
|
-
* The caller is responsible for serializing calls per agent.
|
|
24
|
-
*/
|
|
25
|
-
writeDeployTree(
|
|
26
|
-
agentId: string,
|
|
27
|
-
content: DeployContent,
|
|
28
|
-
): Promise<{ commitSha: string }>;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Produce a packfile from the agent's current deploy ref.
|
|
32
|
-
*/
|
|
33
|
-
createDeployPack(
|
|
34
|
-
agentId: string,
|
|
35
|
-
): Promise<{ pack: Uint8Array; commitSha: string; ref: string }>;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Receive and store a state pack from a sidecar. Indexes the pack
|
|
39
|
-
* objects and updates the ref without materializing a working tree.
|
|
40
|
-
*
|
|
41
|
-
* `repoId.kind` must be `"agent-state"` — this store is per-agent and
|
|
42
|
-
* does not generalize across kinds. The `repoId.id` is used as the
|
|
43
|
-
* agent address internally.
|
|
44
|
-
*/
|
|
45
|
-
receiveStatePack(
|
|
46
|
-
repoId: RepoId,
|
|
47
|
-
pack: Uint8Array,
|
|
48
|
-
ref: string,
|
|
49
|
-
commitSha: string,
|
|
50
|
-
): Promise<void>;
|
|
51
|
-
|
|
52
|
-
/** Resolve the current deploy ref SHA, or null if no deploy exists. */
|
|
53
|
-
getDeployRef(agentId: string): Promise<string | null>;
|
|
54
|
-
|
|
55
|
-
/** Raw 32-byte Ed25519 public key used to sign deploy commits. */
|
|
56
|
-
getSigningPublicKey(): Uint8Array;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Underlying kind-keyed substrate. Exposed so callers that need to
|
|
60
|
-
* operate on non-agent-state kinds (e.g. the asset service writing
|
|
61
|
-
* skill repos) can share the same on-disk root and signing key
|
|
62
|
-
* without spinning up a parallel RepoStore.
|
|
63
|
-
*/
|
|
64
|
-
readonly repoStore: RepoStore;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export function createAgentRepoStore(config: {
|
|
68
|
-
dataDir: string;
|
|
69
|
-
signingKey: { privateKey: Uint8Array; publicKey: Uint8Array };
|
|
70
|
-
}): AgentRepoStore {
|
|
71
|
-
const { dataDir, signingKey } = config;
|
|
72
|
-
|
|
73
|
-
const authorize: AuthorizeFn = (principal, incomingRepoId, ref, action) => {
|
|
74
|
-
switch (incomingRepoId.kind) {
|
|
75
|
-
case "agent-state":
|
|
76
|
-
return agentStateAuthorize(principal, incomingRepoId, ref, action);
|
|
77
|
-
case "skill":
|
|
78
|
-
return skillAuthorize(principal, incomingRepoId, ref, action);
|
|
79
|
-
default: {
|
|
80
|
-
const _exhaustive: never = incomingRepoId.kind;
|
|
81
|
-
return {
|
|
82
|
-
allowed: false,
|
|
83
|
-
reason: `no authorize registered for kind: ${String(_exhaustive)}`,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// The substrate's signingCallback bridges the agent-repo store's
|
|
90
|
-
// raw Ed25519 keypair to the storage layer's per-payload SSHSIG
|
|
91
|
-
// signer. Skill asset genesis commits and agent-state deploy
|
|
92
|
-
// commits both flow through this signer so that signed commits
|
|
93
|
-
// round-trip through the smart-HTTP layer and verify under
|
|
94
|
-
// `git log --show-signature` and `git verify-commit`.
|
|
95
|
-
const signer = async (payload: string) =>
|
|
96
|
-
createSSHSignature(payload, signingKey.privateKey, signingKey.publicKey);
|
|
97
|
-
|
|
98
|
-
const store = createRepoStore({
|
|
99
|
-
dataDir,
|
|
100
|
-
signingKey,
|
|
101
|
-
handlers: {
|
|
102
|
-
"agent-state": agentStateKindHandler,
|
|
103
|
-
skill: skillKindHandler,
|
|
104
|
-
},
|
|
105
|
-
authorize,
|
|
106
|
-
signingCallback: () => signer,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const hub: AgentStateHubPrincipal = { kind: "hub" };
|
|
110
|
-
|
|
111
|
-
function repoId(agentId: string): RepoId {
|
|
112
|
-
return { kind: "agent-state", id: agentId };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
async writeDeployTree(agentId, content) {
|
|
117
|
-
const id = repoId(agentId);
|
|
118
|
-
const files: Record<string, string> = {
|
|
119
|
-
"deploy/prompt.md": content.systemPrompt,
|
|
120
|
-
};
|
|
121
|
-
return store.writeTree(hub, id, AGENT_STATE_DEPLOY_REF, {
|
|
122
|
-
files,
|
|
123
|
-
clearPrefix: "deploy/",
|
|
124
|
-
message: "Update deploy tree",
|
|
125
|
-
});
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
async createDeployPack(agentId) {
|
|
129
|
-
return store.createPack(hub, repoId(agentId), AGENT_STATE_DEPLOY_REF);
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
async receiveStatePack(incomingRepoId, pack, ref, commitSha) {
|
|
133
|
-
if (incomingRepoId.kind !== "agent-state") {
|
|
134
|
-
throw new Error(
|
|
135
|
-
`AgentRepoStore.receiveStatePack requires repoId.kind === "agent-state", got ${JSON.stringify(incomingRepoId.kind)}`,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
const agentId = incomingRepoId.id;
|
|
139
|
-
const id = repoId(agentId);
|
|
140
|
-
const principal: AgentStateSidecarPrincipal = {
|
|
141
|
-
kind: "sidecar",
|
|
142
|
-
agentId,
|
|
143
|
-
};
|
|
144
|
-
const expectedOldSha = await store.resolveRef(principal, id, ref);
|
|
145
|
-
await store.receivePack(
|
|
146
|
-
principal,
|
|
147
|
-
id,
|
|
148
|
-
ref,
|
|
149
|
-
pack,
|
|
150
|
-
commitSha,
|
|
151
|
-
expectedOldSha,
|
|
152
|
-
);
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
async getDeployRef(agentId) {
|
|
156
|
-
return store.resolveRef(hub, repoId(agentId), AGENT_STATE_DEPLOY_REF);
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
getSigningPublicKey() {
|
|
160
|
-
return signingKey.publicKey;
|
|
161
|
-
},
|
|
162
|
-
|
|
163
|
-
repoStore: store,
|
|
164
|
-
};
|
|
165
|
-
}
|