@intx/hub-sessions 0.1.2 → 0.2.2
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 +84 -1
- package/dist/agent-repo.d.ts +89 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +185 -0
- package/dist/asset-service.d.ts +123 -0
- package/dist/asset-service.js +349 -0
- package/dist/available-skills-stanza.d.ts +21 -0
- package/dist/available-skills-stanza.js +32 -0
- package/dist/credential-push.d.ts +32 -0
- package/dist/credential-push.js +85 -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 +357 -0
- package/dist/hub-session-lookups.d.ts +17 -0
- package/dist/hub-session-lookups.js +204 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +122 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -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 +1692 -0
- package/dist/repo-store/subscribe-kind.d.ts +53 -0
- package/dist/repo-store/subscribe-kind.js +179 -0
- package/dist/repo-store/types.d.ts +483 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +235 -0
- package/dist/session-service.js +997 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +288 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-kind.d.ts +21 -0
- package/dist/workflow-kind.js +263 -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 +326 -0
- package/dist/workflow-run-kind.js +2646 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +157 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +134 -0
- package/dist/ws/sidecar-events.js +70 -0
- package/dist/ws/sidecar-handler.d.ts +184 -0
- package/dist/ws/sidecar-handler.js +1603 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +15 -0
- package/dist/ws/sidecar-token-authenticator.js +24 -0
- package/package.json +34 -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
package/src/skill-kind.test.ts
DELETED
|
@@ -1,599 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
skillKindHandler,
|
|
4
|
-
skillFrontmatterSchema,
|
|
5
|
-
skillAuthorize,
|
|
6
|
-
getSkillIndex,
|
|
7
|
-
type SkillIndexEntry,
|
|
8
|
-
} from "./skill-kind";
|
|
9
|
-
import type { Principal, RepoId } from "./repo-store";
|
|
10
|
-
import { type } from "arktype";
|
|
11
|
-
|
|
12
|
-
const REF = "refs/heads/main";
|
|
13
|
-
|
|
14
|
-
function makeReadBlob(
|
|
15
|
-
files: Record<string, string>,
|
|
16
|
-
): (path: string) => Promise<Uint8Array> {
|
|
17
|
-
return async (path) => {
|
|
18
|
-
const body = files[path];
|
|
19
|
-
if (body === undefined) {
|
|
20
|
-
throw new Error(`readBlob: ${path} not found`);
|
|
21
|
-
}
|
|
22
|
-
return new TextEncoder().encode(body);
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function skillMd(frontmatter: Record<string, unknown>, body = ""): string {
|
|
27
|
-
const lines = ["---"];
|
|
28
|
-
for (const [k, v] of Object.entries(frontmatter)) {
|
|
29
|
-
if (typeof v === "string") {
|
|
30
|
-
lines.push(`${k}: ${JSON.stringify(v)}`);
|
|
31
|
-
} else {
|
|
32
|
-
lines.push(`${k}: ${JSON.stringify(v)}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
lines.push("---");
|
|
36
|
-
if (body.length > 0) {
|
|
37
|
-
lines.push(body);
|
|
38
|
-
}
|
|
39
|
-
return lines.join("\n");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function uniqueRepoId(prefix: string): RepoId {
|
|
43
|
-
const id = `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
|
|
44
|
-
return { kind: "skill", id };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe("skillFrontmatterSchema", () => {
|
|
48
|
-
test("accepts valid frontmatter", () => {
|
|
49
|
-
const result = skillFrontmatterSchema({
|
|
50
|
-
name: "good-skill",
|
|
51
|
-
description: "Describes what this skill does.",
|
|
52
|
-
});
|
|
53
|
-
expect(result instanceof type.errors).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("rejects non-kebab name", () => {
|
|
57
|
-
const result = skillFrontmatterSchema({
|
|
58
|
-
name: "BadName",
|
|
59
|
-
description: "ok",
|
|
60
|
-
});
|
|
61
|
-
expect(result instanceof type.errors).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("rejects name longer than 64 characters", () => {
|
|
65
|
-
const result = skillFrontmatterSchema({
|
|
66
|
-
name: "a".repeat(65),
|
|
67
|
-
description: "ok",
|
|
68
|
-
});
|
|
69
|
-
expect(result instanceof type.errors).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("rejects forbidden name 'anthropic'", () => {
|
|
73
|
-
const result = skillFrontmatterSchema({
|
|
74
|
-
name: "anthropic",
|
|
75
|
-
description: "ok",
|
|
76
|
-
});
|
|
77
|
-
expect(result instanceof type.errors).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("rejects forbidden name 'claude'", () => {
|
|
81
|
-
const result = skillFrontmatterSchema({
|
|
82
|
-
name: "claude",
|
|
83
|
-
description: "ok",
|
|
84
|
-
});
|
|
85
|
-
expect(result instanceof type.errors).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("rejects empty description", () => {
|
|
89
|
-
const result = skillFrontmatterSchema({
|
|
90
|
-
name: "ok",
|
|
91
|
-
description: "",
|
|
92
|
-
});
|
|
93
|
-
expect(result instanceof type.errors).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("rejects description longer than 1024 characters", () => {
|
|
97
|
-
const result = skillFrontmatterSchema({
|
|
98
|
-
name: "ok",
|
|
99
|
-
description: "a".repeat(1025),
|
|
100
|
-
});
|
|
101
|
-
expect(result instanceof type.errors).toBe(true);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("rejects description containing XML tags", () => {
|
|
105
|
-
const result = skillFrontmatterSchema({
|
|
106
|
-
name: "ok",
|
|
107
|
-
description: "this has <tag>content</tag>",
|
|
108
|
-
});
|
|
109
|
-
expect(result instanceof type.errors).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("accepts and preserves optional Claude Code superset fields", () => {
|
|
113
|
-
const result = skillFrontmatterSchema({
|
|
114
|
-
name: "ok",
|
|
115
|
-
description: "ok",
|
|
116
|
-
when_to_use: "when greeting",
|
|
117
|
-
"allowed-tools": ["Read", "Edit"],
|
|
118
|
-
paths: ["src/"],
|
|
119
|
-
model: "opus",
|
|
120
|
-
});
|
|
121
|
-
expect(result instanceof type.errors).toBe(false);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe("skillKindHandler.validatePush", () => {
|
|
126
|
-
test("accepts a valid single-skill asset and populates the index after onRefUpdated", async () => {
|
|
127
|
-
const repoId = uniqueRepoId("single");
|
|
128
|
-
const files = {
|
|
129
|
-
"greet/SKILL.md": skillMd({
|
|
130
|
-
name: "greet",
|
|
131
|
-
description: "Greets the user.",
|
|
132
|
-
}),
|
|
133
|
-
};
|
|
134
|
-
const result = await skillKindHandler.validatePush({
|
|
135
|
-
repoId,
|
|
136
|
-
ref: REF,
|
|
137
|
-
topLevelTreePaths: ["greet"],
|
|
138
|
-
readBlob: makeReadBlob(files),
|
|
139
|
-
});
|
|
140
|
-
expect(result.ok).toBe(true);
|
|
141
|
-
|
|
142
|
-
await skillKindHandler.onRefUpdated({
|
|
143
|
-
repoId,
|
|
144
|
-
ref: REF,
|
|
145
|
-
oldSha: null,
|
|
146
|
-
newSha: "deadbeef",
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const entries: SkillIndexEntry[] = getSkillIndex(repoId.id, REF);
|
|
150
|
-
expect(entries).toHaveLength(1);
|
|
151
|
-
const entry = entries[0];
|
|
152
|
-
if (entry === undefined) throw new Error("unreachable");
|
|
153
|
-
expect(entry.name).toBe("greet");
|
|
154
|
-
expect(entry.description).toBe("Greets the user.");
|
|
155
|
-
expect(entry.workspaceSubpath).toBe("greet/");
|
|
156
|
-
expect(entry.frontmatter.name).toBe("greet");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("accepts a multi-skill asset and populates one index entry per skill", async () => {
|
|
160
|
-
const repoId = uniqueRepoId("multi");
|
|
161
|
-
const files = {
|
|
162
|
-
"greet/SKILL.md": skillMd({
|
|
163
|
-
name: "greet",
|
|
164
|
-
description: "Greets the user.",
|
|
165
|
-
}),
|
|
166
|
-
"farewell/SKILL.md": skillMd({
|
|
167
|
-
name: "farewell",
|
|
168
|
-
description: "Says goodbye.",
|
|
169
|
-
when_to_use: "at end of session",
|
|
170
|
-
}),
|
|
171
|
-
};
|
|
172
|
-
const result = await skillKindHandler.validatePush({
|
|
173
|
-
repoId,
|
|
174
|
-
ref: REF,
|
|
175
|
-
topLevelTreePaths: ["greet", "farewell"],
|
|
176
|
-
readBlob: makeReadBlob(files),
|
|
177
|
-
});
|
|
178
|
-
expect(result.ok).toBe(true);
|
|
179
|
-
|
|
180
|
-
await skillKindHandler.onRefUpdated({
|
|
181
|
-
repoId,
|
|
182
|
-
ref: REF,
|
|
183
|
-
oldSha: null,
|
|
184
|
-
newSha: "cafebabe",
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const entries = getSkillIndex(repoId.id, REF);
|
|
188
|
-
expect(entries).toHaveLength(2);
|
|
189
|
-
const names = entries.map((e) => e.name).sort();
|
|
190
|
-
expect(names).toEqual(["farewell", "greet"]);
|
|
191
|
-
|
|
192
|
-
const farewell = entries.find((e) => e.name === "farewell");
|
|
193
|
-
if (farewell === undefined) throw new Error("missing entry");
|
|
194
|
-
expect(farewell.frontmatter.when_to_use).toBe("at end of session");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("accepts an empty asset (no skill subdirectories)", async () => {
|
|
198
|
-
const repoId = uniqueRepoId("empty");
|
|
199
|
-
const result = await skillKindHandler.validatePush({
|
|
200
|
-
repoId,
|
|
201
|
-
ref: REF,
|
|
202
|
-
topLevelTreePaths: [],
|
|
203
|
-
readBlob: makeReadBlob({}),
|
|
204
|
-
});
|
|
205
|
-
expect(result.ok).toBe(true);
|
|
206
|
-
|
|
207
|
-
await skillKindHandler.onRefUpdated({
|
|
208
|
-
repoId,
|
|
209
|
-
ref: REF,
|
|
210
|
-
oldSha: null,
|
|
211
|
-
newSha: "0000",
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
expect(getSkillIndex(repoId.id, REF)).toEqual([]);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("rejects when SKILL.md is missing from a skill subdirectory", async () => {
|
|
218
|
-
const repoId = uniqueRepoId("missing");
|
|
219
|
-
const result = await skillKindHandler.validatePush({
|
|
220
|
-
repoId,
|
|
221
|
-
ref: REF,
|
|
222
|
-
topLevelTreePaths: ["greet"],
|
|
223
|
-
readBlob: makeReadBlob({}),
|
|
224
|
-
});
|
|
225
|
-
expect(result.ok).toBe(false);
|
|
226
|
-
if (result.ok) throw new Error("unreachable");
|
|
227
|
-
expect(result.reason).toMatch(/missing SKILL\.md/);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("rejects when frontmatter.name does not match the directory name", async () => {
|
|
231
|
-
const repoId = uniqueRepoId("mismatch");
|
|
232
|
-
const files = {
|
|
233
|
-
"greet/SKILL.md": skillMd({
|
|
234
|
-
name: "different",
|
|
235
|
-
description: "Mismatch.",
|
|
236
|
-
}),
|
|
237
|
-
};
|
|
238
|
-
const result = await skillKindHandler.validatePush({
|
|
239
|
-
repoId,
|
|
240
|
-
ref: REF,
|
|
241
|
-
topLevelTreePaths: ["greet"],
|
|
242
|
-
readBlob: makeReadBlob(files),
|
|
243
|
-
});
|
|
244
|
-
expect(result.ok).toBe(false);
|
|
245
|
-
if (result.ok) throw new Error("unreachable");
|
|
246
|
-
expect(result.reason).toMatch(/does not match directory name/);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("rejects when the frontmatter name regex fails", async () => {
|
|
250
|
-
const repoId = uniqueRepoId("badname");
|
|
251
|
-
const files = {
|
|
252
|
-
"BadName/SKILL.md": skillMd({
|
|
253
|
-
name: "BadName",
|
|
254
|
-
description: "ok",
|
|
255
|
-
}),
|
|
256
|
-
};
|
|
257
|
-
const result = await skillKindHandler.validatePush({
|
|
258
|
-
repoId,
|
|
259
|
-
ref: REF,
|
|
260
|
-
topLevelTreePaths: ["BadName"],
|
|
261
|
-
readBlob: makeReadBlob(files),
|
|
262
|
-
});
|
|
263
|
-
expect(result.ok).toBe(false);
|
|
264
|
-
if (result.ok) throw new Error("unreachable");
|
|
265
|
-
expect(result.reason).toMatch(/frontmatter is invalid/);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test("rejects when the frontmatter name exceeds 64 characters", async () => {
|
|
269
|
-
const longName = "a".repeat(65);
|
|
270
|
-
const repoId = uniqueRepoId("longname");
|
|
271
|
-
const files = {
|
|
272
|
-
[`${longName}/SKILL.md`]: skillMd({
|
|
273
|
-
name: longName,
|
|
274
|
-
description: "ok",
|
|
275
|
-
}),
|
|
276
|
-
};
|
|
277
|
-
const result = await skillKindHandler.validatePush({
|
|
278
|
-
repoId,
|
|
279
|
-
ref: REF,
|
|
280
|
-
topLevelTreePaths: [longName],
|
|
281
|
-
readBlob: makeReadBlob(files),
|
|
282
|
-
});
|
|
283
|
-
expect(result.ok).toBe(false);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test("rejects when frontmatter name is 'anthropic'", async () => {
|
|
287
|
-
const repoId = uniqueRepoId("anthropic");
|
|
288
|
-
const files = {
|
|
289
|
-
"anthropic/SKILL.md": skillMd({
|
|
290
|
-
name: "anthropic",
|
|
291
|
-
description: "ok",
|
|
292
|
-
}),
|
|
293
|
-
};
|
|
294
|
-
const result = await skillKindHandler.validatePush({
|
|
295
|
-
repoId,
|
|
296
|
-
ref: REF,
|
|
297
|
-
topLevelTreePaths: ["anthropic"],
|
|
298
|
-
readBlob: makeReadBlob(files),
|
|
299
|
-
});
|
|
300
|
-
expect(result.ok).toBe(false);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test("rejects when frontmatter name is 'claude'", async () => {
|
|
304
|
-
const repoId = uniqueRepoId("claude");
|
|
305
|
-
const files = {
|
|
306
|
-
"claude/SKILL.md": skillMd({
|
|
307
|
-
name: "claude",
|
|
308
|
-
description: "ok",
|
|
309
|
-
}),
|
|
310
|
-
};
|
|
311
|
-
const result = await skillKindHandler.validatePush({
|
|
312
|
-
repoId,
|
|
313
|
-
ref: REF,
|
|
314
|
-
topLevelTreePaths: ["claude"],
|
|
315
|
-
readBlob: makeReadBlob(files),
|
|
316
|
-
});
|
|
317
|
-
expect(result.ok).toBe(false);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test("rejects when description is empty", async () => {
|
|
321
|
-
const repoId = uniqueRepoId("emptydesc");
|
|
322
|
-
const files = {
|
|
323
|
-
"ok/SKILL.md": skillMd({
|
|
324
|
-
name: "ok",
|
|
325
|
-
description: "",
|
|
326
|
-
}),
|
|
327
|
-
};
|
|
328
|
-
const result = await skillKindHandler.validatePush({
|
|
329
|
-
repoId,
|
|
330
|
-
ref: REF,
|
|
331
|
-
topLevelTreePaths: ["ok"],
|
|
332
|
-
readBlob: makeReadBlob(files),
|
|
333
|
-
});
|
|
334
|
-
expect(result.ok).toBe(false);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
test("rejects when description exceeds 1024 characters", async () => {
|
|
338
|
-
const repoId = uniqueRepoId("longdesc");
|
|
339
|
-
const files = {
|
|
340
|
-
"ok/SKILL.md": skillMd({
|
|
341
|
-
name: "ok",
|
|
342
|
-
description: "a".repeat(1025),
|
|
343
|
-
}),
|
|
344
|
-
};
|
|
345
|
-
const result = await skillKindHandler.validatePush({
|
|
346
|
-
repoId,
|
|
347
|
-
ref: REF,
|
|
348
|
-
topLevelTreePaths: ["ok"],
|
|
349
|
-
readBlob: makeReadBlob(files),
|
|
350
|
-
});
|
|
351
|
-
expect(result.ok).toBe(false);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
test("rejects when description contains an XML tag", async () => {
|
|
355
|
-
const repoId = uniqueRepoId("xmldesc");
|
|
356
|
-
const files = {
|
|
357
|
-
"ok/SKILL.md": skillMd({
|
|
358
|
-
name: "ok",
|
|
359
|
-
description: "has a <tag>",
|
|
360
|
-
}),
|
|
361
|
-
};
|
|
362
|
-
const result = await skillKindHandler.validatePush({
|
|
363
|
-
repoId,
|
|
364
|
-
ref: REF,
|
|
365
|
-
topLevelTreePaths: ["ok"],
|
|
366
|
-
readBlob: makeReadBlob(files),
|
|
367
|
-
});
|
|
368
|
-
expect(result.ok).toBe(false);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
test("validatePush rejection does not populate the skill index after a subsequent onRefUpdated would be skipped", async () => {
|
|
372
|
-
const repoId = uniqueRepoId("rejected");
|
|
373
|
-
const files = {
|
|
374
|
-
"ok/SKILL.md": skillMd({
|
|
375
|
-
name: "wrongname",
|
|
376
|
-
description: "ok",
|
|
377
|
-
}),
|
|
378
|
-
};
|
|
379
|
-
const result = await skillKindHandler.validatePush({
|
|
380
|
-
repoId,
|
|
381
|
-
ref: REF,
|
|
382
|
-
topLevelTreePaths: ["ok"],
|
|
383
|
-
readBlob: makeReadBlob(files),
|
|
384
|
-
});
|
|
385
|
-
expect(result.ok).toBe(false);
|
|
386
|
-
|
|
387
|
-
// The substrate does not call onRefUpdated when validatePush
|
|
388
|
-
// rejects, so the live index stays empty for this (assetId, ref).
|
|
389
|
-
expect(getSkillIndex(repoId.id, REF)).toEqual([]);
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
describe("skillKindHandler metadata", () => {
|
|
394
|
-
test("declares the skill kind and assets/skill directory prefix", () => {
|
|
395
|
-
expect(skillKindHandler.kind).toBe("skill");
|
|
396
|
-
expect(skillKindHandler.directoryPrefix).toBe("assets/skill");
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
describe("skillAuthorize", () => {
|
|
401
|
-
const SKILL_REPO: RepoId = { kind: "skill", id: "asset-123" };
|
|
402
|
-
const AGENT_STATE_REPO: RepoId = { kind: "agent-state", id: "asset-123" };
|
|
403
|
-
const REF = "refs/heads/main";
|
|
404
|
-
|
|
405
|
-
function farFuture(): number {
|
|
406
|
-
return Date.now() + 60_000;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function userPrincipal(
|
|
410
|
-
overrides: {
|
|
411
|
-
effect?: "allow" | "deny";
|
|
412
|
-
resource?: string;
|
|
413
|
-
grantVerb?: string;
|
|
414
|
-
refPattern?: string;
|
|
415
|
-
actions?: string[];
|
|
416
|
-
expiresAt?: number;
|
|
417
|
-
} = {},
|
|
418
|
-
): Principal {
|
|
419
|
-
return {
|
|
420
|
-
kind: "user",
|
|
421
|
-
principalId: "user-1",
|
|
422
|
-
tenantId: "tenant-1",
|
|
423
|
-
authz: {
|
|
424
|
-
effect: overrides.effect ?? "allow",
|
|
425
|
-
resource: overrides.resource ?? "asset:asset-123",
|
|
426
|
-
grantVerb: overrides.grantVerb ?? "read",
|
|
427
|
-
},
|
|
428
|
-
tokenClaims: {
|
|
429
|
-
refPattern: overrides.refPattern ?? "refs/heads/**",
|
|
430
|
-
actions: overrides.actions ?? ["createPack", "resolveRef"],
|
|
431
|
-
expiresAt: overrides.expiresAt ?? farFuture(),
|
|
432
|
-
},
|
|
433
|
-
} as Principal;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
test("rejects calls when repoId.kind is not skill", () => {
|
|
437
|
-
const r = skillAuthorize(
|
|
438
|
-
{ kind: "hub" } as Principal,
|
|
439
|
-
AGENT_STATE_REPO,
|
|
440
|
-
REF,
|
|
441
|
-
"createPack",
|
|
442
|
-
);
|
|
443
|
-
expect(r.allowed).toBe(false);
|
|
444
|
-
if (r.allowed) throw new Error("unreachable");
|
|
445
|
-
expect(r.reason).toMatch(/non-skill repo/);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
test("hub principal: allowed for any action (regression)", () => {
|
|
449
|
-
for (const action of [
|
|
450
|
-
"init",
|
|
451
|
-
"writeTree",
|
|
452
|
-
"receivePack",
|
|
453
|
-
"createPack",
|
|
454
|
-
"resolveRef",
|
|
455
|
-
] as const) {
|
|
456
|
-
const r = skillAuthorize(
|
|
457
|
-
{ kind: "hub" } as Principal,
|
|
458
|
-
SKILL_REPO,
|
|
459
|
-
REF,
|
|
460
|
-
action,
|
|
461
|
-
);
|
|
462
|
-
expect(r.allowed).toBe(true);
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
test("sidecar principal: createPack / resolveRef allowed (regression)", () => {
|
|
467
|
-
const sidecar = { kind: "sidecar", agentId: "agent-1" } as Principal;
|
|
468
|
-
expect(skillAuthorize(sidecar, SKILL_REPO, REF, "createPack").allowed).toBe(
|
|
469
|
-
true,
|
|
470
|
-
);
|
|
471
|
-
expect(skillAuthorize(sidecar, SKILL_REPO, REF, "resolveRef").allowed).toBe(
|
|
472
|
-
true,
|
|
473
|
-
);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
test("sidecar principal: writeTree / receivePack / init denied (regression)", () => {
|
|
477
|
-
const sidecar = { kind: "sidecar", agentId: "agent-1" } as Principal;
|
|
478
|
-
for (const action of ["init", "writeTree", "receivePack"] as const) {
|
|
479
|
-
const r = skillAuthorize(sidecar, SKILL_REPO, REF, action);
|
|
480
|
-
expect(r.allowed).toBe(false);
|
|
481
|
-
if (r.allowed) throw new Error("unreachable");
|
|
482
|
-
expect(r.reason).toMatch(/sidecars may only read/);
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
test("user principal: allowed when claims and verdict agree", () => {
|
|
487
|
-
const r = skillAuthorize(userPrincipal(), SKILL_REPO, REF, "createPack");
|
|
488
|
-
expect(r.allowed).toBe(true);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
test("user principal: malformed principal is denied with structural reason", () => {
|
|
492
|
-
const badPrincipal = {
|
|
493
|
-
kind: "user",
|
|
494
|
-
principalId: "user-1",
|
|
495
|
-
// missing tenantId, authz, tokenClaims
|
|
496
|
-
} as Principal;
|
|
497
|
-
const r = skillAuthorize(badPrincipal, SKILL_REPO, REF, "createPack");
|
|
498
|
-
expect(r.allowed).toBe(false);
|
|
499
|
-
if (r.allowed) throw new Error("unreachable");
|
|
500
|
-
expect(r.reason).toMatch(/user principal is malformed/);
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
test("user principal: denied when tokenClaims.actions does not include the requested action", () => {
|
|
504
|
-
const r = skillAuthorize(
|
|
505
|
-
userPrincipal({ actions: ["resolveRef"] }),
|
|
506
|
-
SKILL_REPO,
|
|
507
|
-
REF,
|
|
508
|
-
"createPack",
|
|
509
|
-
);
|
|
510
|
-
expect(r.allowed).toBe(false);
|
|
511
|
-
if (r.allowed) throw new Error("unreachable");
|
|
512
|
-
expect(r.reason).toMatch(/token does not grant action createPack/);
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
test("user principal: denied when refPattern does not match the requested ref", () => {
|
|
516
|
-
const r = skillAuthorize(
|
|
517
|
-
userPrincipal({ refPattern: "refs/heads/release-*" }),
|
|
518
|
-
SKILL_REPO,
|
|
519
|
-
REF,
|
|
520
|
-
"createPack",
|
|
521
|
-
);
|
|
522
|
-
expect(r.allowed).toBe(false);
|
|
523
|
-
if (r.allowed) throw new Error("unreachable");
|
|
524
|
-
expect(r.reason).toMatch(/refPattern .* does not match/);
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
test("user principal: denied when the token is expired", () => {
|
|
528
|
-
const r = skillAuthorize(
|
|
529
|
-
userPrincipal({ expiresAt: Date.now() - 1 }),
|
|
530
|
-
SKILL_REPO,
|
|
531
|
-
REF,
|
|
532
|
-
"createPack",
|
|
533
|
-
);
|
|
534
|
-
expect(r.allowed).toBe(false);
|
|
535
|
-
if (r.allowed) throw new Error("unreachable");
|
|
536
|
-
expect(r.reason).toMatch(/token expired/);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
test("user principal: denied when verdict.resource does not target this asset (sanity drift)", () => {
|
|
540
|
-
const r = skillAuthorize(
|
|
541
|
-
userPrincipal({ resource: "asset:other-asset" }),
|
|
542
|
-
SKILL_REPO,
|
|
543
|
-
REF,
|
|
544
|
-
"createPack",
|
|
545
|
-
);
|
|
546
|
-
expect(r.allowed).toBe(false);
|
|
547
|
-
if (r.allowed) throw new Error("unreachable");
|
|
548
|
-
expect(r.reason).toMatch(/authz verdict resource .* does not match/);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test("user principal: denied when verdict.resource has the wrong kind prefix (sanity drift)", () => {
|
|
552
|
-
const r = skillAuthorize(
|
|
553
|
-
userPrincipal({ resource: "agent-state:asset-123" }),
|
|
554
|
-
SKILL_REPO,
|
|
555
|
-
REF,
|
|
556
|
-
"createPack",
|
|
557
|
-
);
|
|
558
|
-
expect(r.allowed).toBe(false);
|
|
559
|
-
if (r.allowed) throw new Error("unreachable");
|
|
560
|
-
expect(r.reason).toMatch(/authz verdict resource .* does not match/);
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
test("user principal: denied when verdict.grantVerb does not match the action's verb (sanity drift)", () => {
|
|
564
|
-
const r = skillAuthorize(
|
|
565
|
-
userPrincipal({ grantVerb: "write" }),
|
|
566
|
-
SKILL_REPO,
|
|
567
|
-
REF,
|
|
568
|
-
"createPack",
|
|
569
|
-
);
|
|
570
|
-
expect(r.allowed).toBe(false);
|
|
571
|
-
if (r.allowed) throw new Error("unreachable");
|
|
572
|
-
expect(r.reason).toMatch(/authz verdict grantVerb .* does not match/);
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
test("user principal: denied when verdict effect is deny even though all sanity checks pass", () => {
|
|
576
|
-
const r = skillAuthorize(
|
|
577
|
-
userPrincipal({ effect: "deny" }),
|
|
578
|
-
SKILL_REPO,
|
|
579
|
-
REF,
|
|
580
|
-
"createPack",
|
|
581
|
-
);
|
|
582
|
-
expect(r.allowed).toBe(false);
|
|
583
|
-
if (r.allowed) throw new Error("unreachable");
|
|
584
|
-
expect(r.reason).toMatch(/authz verdict denied/);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
test("user principal: write action requires write grantVerb and matching claims", () => {
|
|
588
|
-
const r = skillAuthorize(
|
|
589
|
-
userPrincipal({
|
|
590
|
-
actions: ["receivePack"],
|
|
591
|
-
grantVerb: "write",
|
|
592
|
-
}),
|
|
593
|
-
SKILL_REPO,
|
|
594
|
-
REF,
|
|
595
|
-
"receivePack",
|
|
596
|
-
);
|
|
597
|
-
expect(r.allowed).toBe(true);
|
|
598
|
-
});
|
|
599
|
-
});
|