@asm-agent/workflows 0.8.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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @asm-agent/workflows
2
+
3
+ Governed knowledge sources, bounded crawling, moderated ingestion, durable
4
+ research/evaluation/learning workflows, and project-scoped local agent handoff.
5
+
6
+ Remote sources require HTTPS, an explicit host allowlist, robots.txt compliance,
7
+ bounded depth/page counts, owner approval, and content moderation before evidence
8
+ or memory is created. Workflow claiming uses PostgreSQL row locks and fencing
9
+ tokens. Local handoffs require two active sessions in the same project.
10
+
11
+ `BoundedCrawler` performs breadth-first crawling through an injected transport;
12
+ it revalidates robots permission, redirects, and discovered links before content
13
+ can be returned for staging.
package/dist/api.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { GovernedKnowledgeService } from "./knowledge.js";
2
+ export interface KnowledgeApiRequest {
3
+ method: "POST";
4
+ path: "/v1/knowledge/ingest" | "/v1/knowledge/moderate";
5
+ body: Record<string, unknown>;
6
+ }
7
+ export interface KnowledgeApiResponse {
8
+ status: number;
9
+ body: Record<string, unknown>;
10
+ }
11
+ export declare class KnowledgeApi {
12
+ private readonly service;
13
+ constructor(service: GovernedKnowledgeService);
14
+ handle(request: KnowledgeApiRequest): Promise<KnowledgeApiResponse>;
15
+ }
16
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAE/D,MAAM,WAAW,mBAAmB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,sBAAsB,GAAG,wBAAwB,CAAC;IACxD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,qBAAa,YAAY;IACZ,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,wBAAwB,EAAI;IAE5D,MAAM,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAWxE;CACD","sourcesContent":["import type { GovernedKnowledgeService } from \"./knowledge.js\";\n\nexport interface KnowledgeApiRequest {\n\tmethod: \"POST\";\n\tpath: \"/v1/knowledge/ingest\" | \"/v1/knowledge/moderate\";\n\tbody: Record<string, unknown>;\n}\n\nexport interface KnowledgeApiResponse {\n\tstatus: number;\n\tbody: Record<string, unknown>;\n}\n\nexport class KnowledgeApi {\n\tconstructor(private readonly service: GovernedKnowledgeService) {}\n\n\tasync handle(request: KnowledgeApiRequest): Promise<KnowledgeApiResponse> {\n\t\ttry {\n\t\t\tif (request.path === \"/v1/knowledge/ingest\") {\n\t\t\t\tconst value = await this.service.stage(requireStageBody(request.body));\n\t\t\t\treturn { status: 202, body: { status: \"pending_moderation\", ...value } };\n\t\t\t}\n\t\t\tconst value = await this.service.moderate(requireModerationBody(request.body));\n\t\t\treturn { status: 200, body: value };\n\t\t} catch (error) {\n\t\t\treturn { status: 400, body: { error: error instanceof Error ? error.message : String(error) } };\n\t\t}\n\t}\n}\n\nfunction requiredString(body: Record<string, unknown>, key: string): string {\n\tconst value = body[key];\n\tif (typeof value !== \"string\" || !value.trim()) throw new Error(`Missing ${key}`);\n\treturn value;\n}\n\nfunction requireStageBody(body: Record<string, unknown>) {\n\treturn {\n\t\tid: requiredString(body, \"id\"),\n\t\tsourceId: requiredString(body, \"sourceId\"),\n\t\townerId: requiredString(body, \"ownerId\"),\n\t\tprojectId: requiredString(body, \"projectId\"),\n\t\tsourceUri: requiredString(body, \"sourceUri\"),\n\t\tcontent: requiredString(body, \"content\"),\n\t};\n}\n\nfunction requireModerationBody(body: Record<string, unknown>) {\n\tif (typeof body.accept !== \"boolean\") throw new Error(\"Missing accept\");\n\treturn {\n\t\titemId: requiredString(body, \"itemId\"),\n\t\townerId: requiredString(body, \"ownerId\"),\n\t\tprojectId: requiredString(body, \"projectId\"),\n\t\tcontent: requiredString(body, \"content\"),\n\t\taccept: body.accept,\n\t\treason: requiredString(body, \"reason\"),\n\t};\n}\n"]}
package/dist/api.js ADDED
@@ -0,0 +1,48 @@
1
+ export class KnowledgeApi {
2
+ service;
3
+ constructor(service) {
4
+ this.service = service;
5
+ }
6
+ async handle(request) {
7
+ try {
8
+ if (request.path === "/v1/knowledge/ingest") {
9
+ const value = await this.service.stage(requireStageBody(request.body));
10
+ return { status: 202, body: { status: "pending_moderation", ...value } };
11
+ }
12
+ const value = await this.service.moderate(requireModerationBody(request.body));
13
+ return { status: 200, body: value };
14
+ }
15
+ catch (error) {
16
+ return { status: 400, body: { error: error instanceof Error ? error.message : String(error) } };
17
+ }
18
+ }
19
+ }
20
+ function requiredString(body, key) {
21
+ const value = body[key];
22
+ if (typeof value !== "string" || !value.trim())
23
+ throw new Error(`Missing ${key}`);
24
+ return value;
25
+ }
26
+ function requireStageBody(body) {
27
+ return {
28
+ id: requiredString(body, "id"),
29
+ sourceId: requiredString(body, "sourceId"),
30
+ ownerId: requiredString(body, "ownerId"),
31
+ projectId: requiredString(body, "projectId"),
32
+ sourceUri: requiredString(body, "sourceUri"),
33
+ content: requiredString(body, "content"),
34
+ };
35
+ }
36
+ function requireModerationBody(body) {
37
+ if (typeof body.accept !== "boolean")
38
+ throw new Error("Missing accept");
39
+ return {
40
+ itemId: requiredString(body, "itemId"),
41
+ ownerId: requiredString(body, "ownerId"),
42
+ projectId: requiredString(body, "projectId"),
43
+ content: requiredString(body, "content"),
44
+ accept: body.accept,
45
+ reason: requiredString(body, "reason"),
46
+ };
47
+ }
48
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAaA,MAAM,OAAO,YAAY;IACK,OAAO;IAApC,YAA6B,OAAiC,EAAE;uBAAnC,OAAO;IAA6B,CAAC;IAElE,KAAK,CAAC,MAAM,CAAC,OAA4B,EAAiC;QACzE,IAAI,CAAC;YACJ,IAAI,OAAO,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;gBAC7C,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;gBACvE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,oBAAoB,EAAE,GAAG,KAAK,EAAE,EAAE,CAAC;YAC1E,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YAC/E,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QACjG,CAAC;IAAA,CACD;CACD;AAED,SAAS,cAAc,CAAC,IAA6B,EAAE,GAAW,EAAU;IAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC;IAClF,OAAO,KAAK,CAAC;AAAA,CACb;AAED,SAAS,gBAAgB,CAAC,IAA6B,EAAE;IACxD,OAAO;QACN,EAAE,EAAE,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC;QAC9B,QAAQ,EAAE,cAAc,CAAC,IAAI,EAAE,UAAU,CAAC;QAC1C,OAAO,EAAE,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC;QACxC,SAAS,EAAE,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC;QAC5C,SAAS,EAAE,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC;QAC5C,OAAO,EAAE,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC;KACxC,CAAC;AAAA,CACF;AAED,SAAS,qBAAqB,CAAC,IAA6B,EAAE;IAC7D,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACxE,OAAO;QACN,MAAM,EAAE,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC;QACtC,OAAO,EAAE,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC;QACxC,SAAS,EAAE,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC;QAC5C,OAAO,EAAE,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC;QACxC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC;KACtC,CAAC;AAAA,CACF","sourcesContent":["import type { GovernedKnowledgeService } from \"./knowledge.js\";\n\nexport interface KnowledgeApiRequest {\n\tmethod: \"POST\";\n\tpath: \"/v1/knowledge/ingest\" | \"/v1/knowledge/moderate\";\n\tbody: Record<string, unknown>;\n}\n\nexport interface KnowledgeApiResponse {\n\tstatus: number;\n\tbody: Record<string, unknown>;\n}\n\nexport class KnowledgeApi {\n\tconstructor(private readonly service: GovernedKnowledgeService) {}\n\n\tasync handle(request: KnowledgeApiRequest): Promise<KnowledgeApiResponse> {\n\t\ttry {\n\t\t\tif (request.path === \"/v1/knowledge/ingest\") {\n\t\t\t\tconst value = await this.service.stage(requireStageBody(request.body));\n\t\t\t\treturn { status: 202, body: { status: \"pending_moderation\", ...value } };\n\t\t\t}\n\t\t\tconst value = await this.service.moderate(requireModerationBody(request.body));\n\t\t\treturn { status: 200, body: value };\n\t\t} catch (error) {\n\t\t\treturn { status: 400, body: { error: error instanceof Error ? error.message : String(error) } };\n\t\t}\n\t}\n}\n\nfunction requiredString(body: Record<string, unknown>, key: string): string {\n\tconst value = body[key];\n\tif (typeof value !== \"string\" || !value.trim()) throw new Error(`Missing ${key}`);\n\treturn value;\n}\n\nfunction requireStageBody(body: Record<string, unknown>) {\n\treturn {\n\t\tid: requiredString(body, \"id\"),\n\t\tsourceId: requiredString(body, \"sourceId\"),\n\t\townerId: requiredString(body, \"ownerId\"),\n\t\tprojectId: requiredString(body, \"projectId\"),\n\t\tsourceUri: requiredString(body, \"sourceUri\"),\n\t\tcontent: requiredString(body, \"content\"),\n\t};\n}\n\nfunction requireModerationBody(body: Record<string, unknown>) {\n\tif (typeof body.accept !== \"boolean\") throw new Error(\"Missing accept\");\n\treturn {\n\t\titemId: requiredString(body, \"itemId\"),\n\t\townerId: requiredString(body, \"ownerId\"),\n\t\tprojectId: requiredString(body, \"projectId\"),\n\t\tcontent: requiredString(body, \"content\"),\n\t\taccept: body.accept,\n\t\treason: requiredString(body, \"reason\"),\n\t};\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { KnowledgeApi, KnowledgeApiResponse } from "./api.js";
2
+ export declare class KnowledgeBrowserClient {
3
+ private readonly api;
4
+ constructor(api: KnowledgeApi);
5
+ submitForModeration(input: {
6
+ id: string;
7
+ sourceId: string;
8
+ ownerId: string;
9
+ projectId: string;
10
+ sourceUri: string;
11
+ content: string;
12
+ }): Promise<KnowledgeApiResponse>;
13
+ moderate(input: {
14
+ itemId: string;
15
+ ownerId: string;
16
+ projectId: string;
17
+ content: string;
18
+ accept: boolean;
19
+ reason: string;
20
+ }): Promise<KnowledgeApiResponse>;
21
+ }
22
+ //# sourceMappingURL=browser-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-client.d.ts","sourceRoot":"","sources":["../src/browser-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAEnE,qBAAa,sBAAsB;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAhC,YAA6B,GAAG,EAAE,YAAY,EAAI;IAE5C,mBAAmB,CAAC,KAAK,EAAE;QAChC,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAEhC;IAEK,QAAQ,CAAC,KAAK,EAAE;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAEhC;CACD","sourcesContent":["import type { KnowledgeApi, KnowledgeApiResponse } from \"./api.js\";\n\nexport class KnowledgeBrowserClient {\n\tconstructor(private readonly api: KnowledgeApi) {}\n\n\tasync submitForModeration(input: {\n\t\tid: string;\n\t\tsourceId: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tsourceUri: string;\n\t\tcontent: string;\n\t}): Promise<KnowledgeApiResponse> {\n\t\treturn this.api.handle({ method: \"POST\", path: \"/v1/knowledge/ingest\", body: input });\n\t}\n\n\tasync moderate(input: {\n\t\titemId: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tcontent: string;\n\t\taccept: boolean;\n\t\treason: string;\n\t}): Promise<KnowledgeApiResponse> {\n\t\treturn this.api.handle({ method: \"POST\", path: \"/v1/knowledge/moderate\", body: input });\n\t}\n}\n"]}
@@ -0,0 +1,13 @@
1
+ export class KnowledgeBrowserClient {
2
+ api;
3
+ constructor(api) {
4
+ this.api = api;
5
+ }
6
+ async submitForModeration(input) {
7
+ return this.api.handle({ method: "POST", path: "/v1/knowledge/ingest", body: input });
8
+ }
9
+ async moderate(input) {
10
+ return this.api.handle({ method: "POST", path: "/v1/knowledge/moderate", body: input });
11
+ }
12
+ }
13
+ //# sourceMappingURL=browser-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-client.js","sourceRoot":"","sources":["../src/browser-client.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,sBAAsB;IACL,GAAG;IAAhC,YAA6B,GAAiB,EAAE;mBAAnB,GAAG;IAAiB,CAAC;IAElD,KAAK,CAAC,mBAAmB,CAAC,KAOzB,EAAiC;QACjC,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAAA,CACtF;IAED,KAAK,CAAC,QAAQ,CAAC,KAOd,EAAiC;QACjC,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,wBAAwB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAAA,CACxF;CACD","sourcesContent":["import type { KnowledgeApi, KnowledgeApiResponse } from \"./api.js\";\n\nexport class KnowledgeBrowserClient {\n\tconstructor(private readonly api: KnowledgeApi) {}\n\n\tasync submitForModeration(input: {\n\t\tid: string;\n\t\tsourceId: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tsourceUri: string;\n\t\tcontent: string;\n\t}): Promise<KnowledgeApiResponse> {\n\t\treturn this.api.handle({ method: \"POST\", path: \"/v1/knowledge/ingest\", body: input });\n\t}\n\n\tasync moderate(input: {\n\t\titemId: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tcontent: string;\n\t\taccept: boolean;\n\t\treason: string;\n\t}): Promise<KnowledgeApiResponse> {\n\t\treturn this.api.handle({ method: \"POST\", path: \"/v1/knowledge/moderate\", body: input });\n\t}\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import type { CrawlPolicy } from "./types.js";
2
+ export interface CrawledPage {
3
+ uri: string;
4
+ content: string;
5
+ depth: number;
6
+ }
7
+ export interface CrawlFetchResult {
8
+ finalUri: string;
9
+ content: string;
10
+ links: string[];
11
+ }
12
+ export interface CrawlTransport {
13
+ isAllowedByRobots(uri: string): Promise<boolean>;
14
+ fetch(uri: string): Promise<CrawlFetchResult>;
15
+ }
16
+ export declare class BoundedCrawler {
17
+ private readonly transport;
18
+ constructor(transport: CrawlTransport);
19
+ crawl(startUri: string, policy: CrawlPolicy): Promise<CrawledPage[]>;
20
+ }
21
+ //# sourceMappingURL=crawler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crawler.d.ts","sourceRoot":"","sources":["../src/crawler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC9C;AAED,qBAAa,cAAc;IACd,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAtC,YAA6B,SAAS,EAAE,cAAc,EAAI;IAEpD,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAuBzE;CACD","sourcesContent":["import type { CrawlPolicy } from \"./types.js\";\n\nexport interface CrawledPage {\n\turi: string;\n\tcontent: string;\n\tdepth: number;\n}\n\nexport interface CrawlFetchResult {\n\tfinalUri: string;\n\tcontent: string;\n\tlinks: string[];\n}\n\nexport interface CrawlTransport {\n\tisAllowedByRobots(uri: string): Promise<boolean>;\n\tfetch(uri: string): Promise<CrawlFetchResult>;\n}\n\nexport class BoundedCrawler {\n\tconstructor(private readonly transport: CrawlTransport) {}\n\n\tasync crawl(startUri: string, policy: CrawlPolicy): Promise<CrawledPage[]> {\n\t\tvalidatePolicy(startUri, policy);\n\t\tconst queue: Array<{ uri: string; depth: number }> = [{ uri: normalize(startUri), depth: 0 }];\n\t\tconst seen = new Set<string>();\n\t\tconst pages: CrawledPage[] = [];\n\t\twhile (queue.length > 0 && pages.length < policy.maxPages) {\n\t\t\tconst target = queue.shift();\n\t\t\tif (!target || seen.has(target.uri) || target.depth > policy.maxDepth) continue;\n\t\t\tseen.add(target.uri);\n\t\t\tassertAllowed(target.uri, policy);\n\t\t\tif (!(await this.transport.isAllowedByRobots(target.uri))) continue;\n\t\t\tconst fetched = await this.transport.fetch(target.uri);\n\t\t\tconst finalUri = normalize(fetched.finalUri);\n\t\t\tassertAllowed(finalUri, policy);\n\t\t\tpages.push({ uri: finalUri, content: fetched.content, depth: target.depth });\n\t\t\tif (target.depth === policy.maxDepth) continue;\n\t\t\tfor (const link of fetched.links) {\n\t\t\t\tconst resolved = normalize(new URL(link, finalUri).toString());\n\t\t\t\tif (isAllowed(resolved, policy) && !seen.has(resolved))\n\t\t\t\t\tqueue.push({ uri: resolved, depth: target.depth + 1 });\n\t\t\t}\n\t\t}\n\t\treturn pages;\n\t}\n}\n\nfunction validatePolicy(startUri: string, policy: CrawlPolicy): void {\n\tif (policy.respectRobotsTxt !== true) throw new Error(\"Crawler requires robots.txt enforcement\");\n\tif (!Number.isInteger(policy.maxDepth) || policy.maxDepth < 0 || policy.maxDepth > 5)\n\t\tthrow new Error(\"Crawl maxDepth must be between 0 and 5\");\n\tif (!Number.isInteger(policy.maxPages) || policy.maxPages < 1 || policy.maxPages > 1_000)\n\t\tthrow new Error(\"Crawl maxPages must be between 1 and 1000\");\n\tassertAllowed(startUri, policy);\n}\n\nfunction assertAllowed(uri: string, policy: CrawlPolicy): void {\n\tif (!isAllowed(uri, policy)) throw new Error(\"Crawler target is outside the HTTPS host allowlist\");\n}\n\nfunction isAllowed(uri: string, policy: CrawlPolicy): boolean {\n\tconst parsed = new URL(uri);\n\treturn parsed.protocol === \"https:\" && policy.allowedHosts.includes(parsed.hostname);\n}\n\nfunction normalize(uri: string): string {\n\tconst parsed = new URL(uri);\n\tparsed.hash = \"\";\n\treturn parsed.toString();\n}\n"]}
@@ -0,0 +1,56 @@
1
+ export class BoundedCrawler {
2
+ transport;
3
+ constructor(transport) {
4
+ this.transport = transport;
5
+ }
6
+ async crawl(startUri, policy) {
7
+ validatePolicy(startUri, policy);
8
+ const queue = [{ uri: normalize(startUri), depth: 0 }];
9
+ const seen = new Set();
10
+ const pages = [];
11
+ while (queue.length > 0 && pages.length < policy.maxPages) {
12
+ const target = queue.shift();
13
+ if (!target || seen.has(target.uri) || target.depth > policy.maxDepth)
14
+ continue;
15
+ seen.add(target.uri);
16
+ assertAllowed(target.uri, policy);
17
+ if (!(await this.transport.isAllowedByRobots(target.uri)))
18
+ continue;
19
+ const fetched = await this.transport.fetch(target.uri);
20
+ const finalUri = normalize(fetched.finalUri);
21
+ assertAllowed(finalUri, policy);
22
+ pages.push({ uri: finalUri, content: fetched.content, depth: target.depth });
23
+ if (target.depth === policy.maxDepth)
24
+ continue;
25
+ for (const link of fetched.links) {
26
+ const resolved = normalize(new URL(link, finalUri).toString());
27
+ if (isAllowed(resolved, policy) && !seen.has(resolved))
28
+ queue.push({ uri: resolved, depth: target.depth + 1 });
29
+ }
30
+ }
31
+ return pages;
32
+ }
33
+ }
34
+ function validatePolicy(startUri, policy) {
35
+ if (policy.respectRobotsTxt !== true)
36
+ throw new Error("Crawler requires robots.txt enforcement");
37
+ if (!Number.isInteger(policy.maxDepth) || policy.maxDepth < 0 || policy.maxDepth > 5)
38
+ throw new Error("Crawl maxDepth must be between 0 and 5");
39
+ if (!Number.isInteger(policy.maxPages) || policy.maxPages < 1 || policy.maxPages > 1_000)
40
+ throw new Error("Crawl maxPages must be between 1 and 1000");
41
+ assertAllowed(startUri, policy);
42
+ }
43
+ function assertAllowed(uri, policy) {
44
+ if (!isAllowed(uri, policy))
45
+ throw new Error("Crawler target is outside the HTTPS host allowlist");
46
+ }
47
+ function isAllowed(uri, policy) {
48
+ const parsed = new URL(uri);
49
+ return parsed.protocol === "https:" && policy.allowedHosts.includes(parsed.hostname);
50
+ }
51
+ function normalize(uri) {
52
+ const parsed = new URL(uri);
53
+ parsed.hash = "";
54
+ return parsed.toString();
55
+ }
56
+ //# sourceMappingURL=crawler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crawler.js","sourceRoot":"","sources":["../src/crawler.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CrawlPolicy } from \"./types.js\";\n\nexport interface CrawledPage {\n\turi: string;\n\tcontent: string;\n\tdepth: number;\n}\n\nexport interface CrawlFetchResult {\n\tfinalUri: string;\n\tcontent: string;\n\tlinks: string[];\n}\n\nexport interface CrawlTransport {\n\tisAllowedByRobots(uri: string): Promise<boolean>;\n\tfetch(uri: string): Promise<CrawlFetchResult>;\n}\n\nexport class BoundedCrawler {\n\tconstructor(private readonly transport: CrawlTransport) {}\n\n\tasync crawl(startUri: string, policy: CrawlPolicy): Promise<CrawledPage[]> {\n\t\tvalidatePolicy(startUri, policy);\n\t\tconst queue: Array<{ uri: string; depth: number }> = [{ uri: normalize(startUri), depth: 0 }];\n\t\tconst seen = new Set<string>();\n\t\tconst pages: CrawledPage[] = [];\n\t\twhile (queue.length > 0 && pages.length < policy.maxPages) {\n\t\t\tconst target = queue.shift();\n\t\t\tif (!target || seen.has(target.uri) || target.depth > policy.maxDepth) continue;\n\t\t\tseen.add(target.uri);\n\t\t\tassertAllowed(target.uri, policy);\n\t\t\tif (!(await this.transport.isAllowedByRobots(target.uri))) continue;\n\t\t\tconst fetched = await this.transport.fetch(target.uri);\n\t\t\tconst finalUri = normalize(fetched.finalUri);\n\t\t\tassertAllowed(finalUri, policy);\n\t\t\tpages.push({ uri: finalUri, content: fetched.content, depth: target.depth });\n\t\t\tif (target.depth === policy.maxDepth) continue;\n\t\t\tfor (const link of fetched.links) {\n\t\t\t\tconst resolved = normalize(new URL(link, finalUri).toString());\n\t\t\t\tif (isAllowed(resolved, policy) && !seen.has(resolved))\n\t\t\t\t\tqueue.push({ uri: resolved, depth: target.depth + 1 });\n\t\t\t}\n\t\t}\n\t\treturn pages;\n\t}\n}\n\nfunction validatePolicy(startUri: string, policy: CrawlPolicy): void {\n\tif (policy.respectRobotsTxt !== true) throw new Error(\"Crawler requires robots.txt enforcement\");\n\tif (!Number.isInteger(policy.maxDepth) || policy.maxDepth < 0 || policy.maxDepth > 5)\n\t\tthrow new Error(\"Crawl maxDepth must be between 0 and 5\");\n\tif (!Number.isInteger(policy.maxPages) || policy.maxPages < 1 || policy.maxPages > 1_000)\n\t\tthrow new Error(\"Crawl maxPages must be between 1 and 1000\");\n\tassertAllowed(startUri, policy);\n}\n\nfunction assertAllowed(uri: string, policy: CrawlPolicy): void {\n\tif (!isAllowed(uri, policy)) throw new Error(\"Crawler target is outside the HTTPS host allowlist\");\n}\n\nfunction isAllowed(uri: string, policy: CrawlPolicy): boolean {\n\tconst parsed = new URL(uri);\n\treturn parsed.protocol === \"https:\" && policy.allowedHosts.includes(parsed.hostname);\n}\n\nfunction normalize(uri: string): string {\n\tconst parsed = new URL(uri);\n\tparsed.hash = \"\";\n\treturn parsed.toString();\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import type { PostgresStore } from "@asm-agent/postgres";
2
+ import type { LocalHandoff } from "./types.js";
3
+ export declare class LocalAgentHandoffService {
4
+ private readonly database;
5
+ private readonly repository;
6
+ constructor(database: PostgresStore);
7
+ offer(input: {
8
+ id: string;
9
+ ownerId: string;
10
+ projectId: string;
11
+ fromSessionId: string;
12
+ toSessionId: string;
13
+ fromAgentId: string;
14
+ toAgentId: string;
15
+ objective: string;
16
+ payload?: Record<string, unknown>;
17
+ evidenceIds?: string[];
18
+ }): Promise<void>;
19
+ accept(id: string, toAgentId: string, toSessionId: string): Promise<LocalHandoff>;
20
+ }
21
+ //# sourceMappingURL=handoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff.d.ts","sourceRoot":"","sources":["../src/handoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,qBAAa,wBAAwB;IAGxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAFrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAEhD,YAA6B,QAAQ,EAAE,aAAa,EAEnD;IAEK,KAAK,CAAC,KAAK,EAAE;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAClC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;KACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmChB;IAEK,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAUtF;CACD","sourcesContent":["import type { PostgresStore } from \"@asm-agent/postgres\";\nimport { WorkflowRepository } from \"./repository.js\";\nimport type { LocalHandoff } from \"./types.js\";\n\nexport class LocalAgentHandoffService {\n\tprivate readonly repository: WorkflowRepository;\n\n\tconstructor(private readonly database: PostgresStore) {\n\t\tthis.repository = new WorkflowRepository(database);\n\t}\n\n\tasync offer(input: {\n\t\tid: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tfromSessionId: string;\n\t\ttoSessionId: string;\n\t\tfromAgentId: string;\n\t\ttoAgentId: string;\n\t\tobjective: string;\n\t\tpayload?: Record<string, unknown>;\n\t\tevidenceIds?: string[];\n\t}): Promise<void> {\n\t\tif (!input.objective.trim()) throw new Error(\"Handoff objective must not be empty\");\n\t\tif (input.fromAgentId === input.toAgentId) throw new Error(\"Handoff requires distinct local agent identities\");\n\t\tawait this.database.transaction(async (client) => {\n\t\t\tconst sessions = await client.query<{ id: string }>(\n\t\t\t\t\"SELECT id FROM asm_agent.sessions WHERE id=ANY($1::uuid[]) AND project_id=$2 AND status='active'\",\n\t\t\t\t[[input.fromSessionId, input.toSessionId], input.projectId],\n\t\t\t);\n\t\t\tif (sessions.rows.length !== 2) throw new Error(\"Handoff sessions must be active in the same project\");\n\t\t\tif ((input.evidenceIds?.length ?? 0) > 0) {\n\t\t\t\tconst evidence = await client.query(\n\t\t\t\t\t\"SELECT id FROM asm_agent.evidence_snapshots WHERE id=ANY($1::uuid[]) AND owner_id=$2 AND project_id=$3 AND revoked_at IS NULL\",\n\t\t\t\t\t[input.evidenceIds, input.ownerId, input.projectId],\n\t\t\t\t);\n\t\t\t\tif (evidence.rows.length !== new Set(input.evidenceIds).size)\n\t\t\t\t\tthrow new Error(\"Handoff evidence is revoked or outside project scope\");\n\t\t\t}\n\t\t\tawait client.query(\n\t\t\t\t`INSERT INTO asm_agent.local_agent_handoffs\n\t\t\t\t (id,owner_id,project_id,from_session_id,to_session_id,from_agent_id,to_agent_id,status,objective,payload,evidence_ids)\n\t\t\t\t VALUES ($1,$2,$3,$4,$5,$6,$7,'offered',$8,$9::jsonb,$10)`,\n\t\t\t\t[\n\t\t\t\t\tinput.id,\n\t\t\t\t\tinput.ownerId,\n\t\t\t\t\tinput.projectId,\n\t\t\t\t\tinput.fromSessionId,\n\t\t\t\t\tinput.toSessionId,\n\t\t\t\t\tinput.fromAgentId,\n\t\t\t\t\tinput.toAgentId,\n\t\t\t\t\tinput.objective.trim(),\n\t\t\t\t\tJSON.stringify(input.payload ?? {}),\n\t\t\t\t\tinput.evidenceIds ?? [],\n\t\t\t\t],\n\t\t\t);\n\t\t});\n\t}\n\n\tasync accept(id: string, toAgentId: string, toSessionId: string): Promise<LocalHandoff> {\n\t\tconst result = await this.database.pool.query(\n\t\t\t`UPDATE asm_agent.local_agent_handoffs SET status='accepted',accepted_at=clock_timestamp()\n\t\t\t WHERE id=$1 AND to_agent_id=$2 AND to_session_id=$3 AND status='offered' RETURNING id`,\n\t\t\t[id, toAgentId, toSessionId],\n\t\t);\n\t\tif ((result.rowCount ?? 0) !== 1) throw new Error(\"Handoff cannot be accepted by this agent session\");\n\t\tconst handoff = await this.repository.getHandoff(id);\n\t\tif (!handoff) throw new Error(\"Accepted handoff disappeared\");\n\t\treturn handoff;\n\t}\n}\n"]}
@@ -0,0 +1,50 @@
1
+ import { WorkflowRepository } from "./repository.js";
2
+ export class LocalAgentHandoffService {
3
+ database;
4
+ repository;
5
+ constructor(database) {
6
+ this.database = database;
7
+ this.repository = new WorkflowRepository(database);
8
+ }
9
+ async offer(input) {
10
+ if (!input.objective.trim())
11
+ throw new Error("Handoff objective must not be empty");
12
+ if (input.fromAgentId === input.toAgentId)
13
+ throw new Error("Handoff requires distinct local agent identities");
14
+ await this.database.transaction(async (client) => {
15
+ const sessions = await client.query("SELECT id FROM asm_agent.sessions WHERE id=ANY($1::uuid[]) AND project_id=$2 AND status='active'", [[input.fromSessionId, input.toSessionId], input.projectId]);
16
+ if (sessions.rows.length !== 2)
17
+ throw new Error("Handoff sessions must be active in the same project");
18
+ if ((input.evidenceIds?.length ?? 0) > 0) {
19
+ const evidence = await client.query("SELECT id FROM asm_agent.evidence_snapshots WHERE id=ANY($1::uuid[]) AND owner_id=$2 AND project_id=$3 AND revoked_at IS NULL", [input.evidenceIds, input.ownerId, input.projectId]);
20
+ if (evidence.rows.length !== new Set(input.evidenceIds).size)
21
+ throw new Error("Handoff evidence is revoked or outside project scope");
22
+ }
23
+ await client.query(`INSERT INTO asm_agent.local_agent_handoffs
24
+ (id,owner_id,project_id,from_session_id,to_session_id,from_agent_id,to_agent_id,status,objective,payload,evidence_ids)
25
+ VALUES ($1,$2,$3,$4,$5,$6,$7,'offered',$8,$9::jsonb,$10)`, [
26
+ input.id,
27
+ input.ownerId,
28
+ input.projectId,
29
+ input.fromSessionId,
30
+ input.toSessionId,
31
+ input.fromAgentId,
32
+ input.toAgentId,
33
+ input.objective.trim(),
34
+ JSON.stringify(input.payload ?? {}),
35
+ input.evidenceIds ?? [],
36
+ ]);
37
+ });
38
+ }
39
+ async accept(id, toAgentId, toSessionId) {
40
+ const result = await this.database.pool.query(`UPDATE asm_agent.local_agent_handoffs SET status='accepted',accepted_at=clock_timestamp()
41
+ WHERE id=$1 AND to_agent_id=$2 AND to_session_id=$3 AND status='offered' RETURNING id`, [id, toAgentId, toSessionId]);
42
+ if ((result.rowCount ?? 0) !== 1)
43
+ throw new Error("Handoff cannot be accepted by this agent session");
44
+ const handoff = await this.repository.getHandoff(id);
45
+ if (!handoff)
46
+ throw new Error("Accepted handoff disappeared");
47
+ return handoff;
48
+ }
49
+ }
50
+ //# sourceMappingURL=handoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff.js","sourceRoot":"","sources":["../src/handoff.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGrD,MAAM,OAAO,wBAAwB;IAGP,QAAQ;IAFpB,UAAU,CAAqB;IAEhD,YAA6B,QAAuB,EAAE;wBAAzB,QAAQ;QACpC,IAAI,CAAC,UAAU,GAAG,IAAI,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAAA,CACnD;IAED,KAAK,CAAC,KAAK,CAAC,KAWX,EAAiB;QACjB,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACpF,IAAI,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAC/G,MAAM,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;YACjD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAClC,kGAAkG,EAClG,CAAC,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAC3D,CAAC;YACF,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;YACvG,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAClC,+HAA+H,EAC/H,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CACnD,CAAC;gBACF,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,IAAI;oBAC3D,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;YAC1E,CAAC;YACD,MAAM,MAAM,CAAC,KAAK,CACjB;;8DAE0D,EAC1D;gBACC,KAAK,CAAC,EAAE;gBACR,KAAK,CAAC,OAAO;gBACb,KAAK,CAAC,SAAS;gBACf,KAAK,CAAC,aAAa;gBACnB,KAAK,CAAC,WAAW;gBACjB,KAAK,CAAC,WAAW;gBACjB,KAAK,CAAC,SAAS;gBACf,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE;gBACtB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;gBACnC,KAAK,CAAC,WAAW,IAAI,EAAE;aACvB,CACD,CAAC;QAAA,CACF,CAAC,CAAC;IAAA,CACH;IAED,KAAK,CAAC,MAAM,CAAC,EAAU,EAAE,SAAiB,EAAE,WAAmB,EAAyB;QACvF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAC5C;0FACuF,EACvF,CAAC,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC,CAC5B,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QACtG,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QACrD,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAC9D,OAAO,OAAO,CAAC;IAAA,CACf;CACD","sourcesContent":["import type { PostgresStore } from \"@asm-agent/postgres\";\nimport { WorkflowRepository } from \"./repository.js\";\nimport type { LocalHandoff } from \"./types.js\";\n\nexport class LocalAgentHandoffService {\n\tprivate readonly repository: WorkflowRepository;\n\n\tconstructor(private readonly database: PostgresStore) {\n\t\tthis.repository = new WorkflowRepository(database);\n\t}\n\n\tasync offer(input: {\n\t\tid: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tfromSessionId: string;\n\t\ttoSessionId: string;\n\t\tfromAgentId: string;\n\t\ttoAgentId: string;\n\t\tobjective: string;\n\t\tpayload?: Record<string, unknown>;\n\t\tevidenceIds?: string[];\n\t}): Promise<void> {\n\t\tif (!input.objective.trim()) throw new Error(\"Handoff objective must not be empty\");\n\t\tif (input.fromAgentId === input.toAgentId) throw new Error(\"Handoff requires distinct local agent identities\");\n\t\tawait this.database.transaction(async (client) => {\n\t\t\tconst sessions = await client.query<{ id: string }>(\n\t\t\t\t\"SELECT id FROM asm_agent.sessions WHERE id=ANY($1::uuid[]) AND project_id=$2 AND status='active'\",\n\t\t\t\t[[input.fromSessionId, input.toSessionId], input.projectId],\n\t\t\t);\n\t\t\tif (sessions.rows.length !== 2) throw new Error(\"Handoff sessions must be active in the same project\");\n\t\t\tif ((input.evidenceIds?.length ?? 0) > 0) {\n\t\t\t\tconst evidence = await client.query(\n\t\t\t\t\t\"SELECT id FROM asm_agent.evidence_snapshots WHERE id=ANY($1::uuid[]) AND owner_id=$2 AND project_id=$3 AND revoked_at IS NULL\",\n\t\t\t\t\t[input.evidenceIds, input.ownerId, input.projectId],\n\t\t\t\t);\n\t\t\t\tif (evidence.rows.length !== new Set(input.evidenceIds).size)\n\t\t\t\t\tthrow new Error(\"Handoff evidence is revoked or outside project scope\");\n\t\t\t}\n\t\t\tawait client.query(\n\t\t\t\t`INSERT INTO asm_agent.local_agent_handoffs\n\t\t\t\t (id,owner_id,project_id,from_session_id,to_session_id,from_agent_id,to_agent_id,status,objective,payload,evidence_ids)\n\t\t\t\t VALUES ($1,$2,$3,$4,$5,$6,$7,'offered',$8,$9::jsonb,$10)`,\n\t\t\t\t[\n\t\t\t\t\tinput.id,\n\t\t\t\t\tinput.ownerId,\n\t\t\t\t\tinput.projectId,\n\t\t\t\t\tinput.fromSessionId,\n\t\t\t\t\tinput.toSessionId,\n\t\t\t\t\tinput.fromAgentId,\n\t\t\t\t\tinput.toAgentId,\n\t\t\t\t\tinput.objective.trim(),\n\t\t\t\t\tJSON.stringify(input.payload ?? {}),\n\t\t\t\t\tinput.evidenceIds ?? [],\n\t\t\t\t],\n\t\t\t);\n\t\t});\n\t}\n\n\tasync accept(id: string, toAgentId: string, toSessionId: string): Promise<LocalHandoff> {\n\t\tconst result = await this.database.pool.query(\n\t\t\t`UPDATE asm_agent.local_agent_handoffs SET status='accepted',accepted_at=clock_timestamp()\n\t\t\t WHERE id=$1 AND to_agent_id=$2 AND to_session_id=$3 AND status='offered' RETURNING id`,\n\t\t\t[id, toAgentId, toSessionId],\n\t\t);\n\t\tif ((result.rowCount ?? 0) !== 1) throw new Error(\"Handoff cannot be accepted by this agent session\");\n\t\tconst handoff = await this.repository.getHandoff(id);\n\t\tif (!handoff) throw new Error(\"Accepted handoff disappeared\");\n\t\treturn handoff;\n\t}\n}\n"]}
@@ -0,0 +1,8 @@
1
+ export { KnowledgeApi, type KnowledgeApiRequest, type KnowledgeApiResponse } from "./api.js";
2
+ export { KnowledgeBrowserClient } from "./browser-client.js";
3
+ export { BoundedCrawler, type CrawledPage, type CrawlFetchResult, type CrawlTransport } from "./crawler.js";
4
+ export { LocalAgentHandoffService } from "./handoff.js";
5
+ export { GovernedKnowledgeService } from "./knowledge.js";
6
+ export { WorkflowRepository } from "./repository.js";
7
+ export type { CrawlPolicy, KnowledgeSourceKind, KnowledgeSourceStatus, LocalHandoff, WorkflowClaim, WorkflowKind, WorkflowStatus, } from "./types.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAC7F,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5G,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,YAAY,EACX,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,cAAc,GACd,MAAM,YAAY,CAAC","sourcesContent":["export { KnowledgeApi, type KnowledgeApiRequest, type KnowledgeApiResponse } from \"./api.js\";\nexport { KnowledgeBrowserClient } from \"./browser-client.js\";\nexport { BoundedCrawler, type CrawledPage, type CrawlFetchResult, type CrawlTransport } from \"./crawler.js\";\nexport { LocalAgentHandoffService } from \"./handoff.js\";\nexport { GovernedKnowledgeService } from \"./knowledge.js\";\nexport { WorkflowRepository } from \"./repository.js\";\nexport type {\n\tCrawlPolicy,\n\tKnowledgeSourceKind,\n\tKnowledgeSourceStatus,\n\tLocalHandoff,\n\tWorkflowClaim,\n\tWorkflowKind,\n\tWorkflowStatus,\n} from \"./types.js\";\n"]}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { KnowledgeApi } from "./api.js";
2
+ export { KnowledgeBrowserClient } from "./browser-client.js";
3
+ export { BoundedCrawler } from "./crawler.js";
4
+ export { LocalAgentHandoffService } from "./handoff.js";
5
+ export { GovernedKnowledgeService } from "./knowledge.js";
6
+ export { WorkflowRepository } from "./repository.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuD,MAAM,UAAU,CAAC;AAC7F,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAgE,MAAM,cAAc,CAAC;AAC5G,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["export { KnowledgeApi, type KnowledgeApiRequest, type KnowledgeApiResponse } from \"./api.js\";\nexport { KnowledgeBrowserClient } from \"./browser-client.js\";\nexport { BoundedCrawler, type CrawledPage, type CrawlFetchResult, type CrawlTransport } from \"./crawler.js\";\nexport { LocalAgentHandoffService } from \"./handoff.js\";\nexport { GovernedKnowledgeService } from \"./knowledge.js\";\nexport { WorkflowRepository } from \"./repository.js\";\nexport type {\n\tCrawlPolicy,\n\tKnowledgeSourceKind,\n\tKnowledgeSourceStatus,\n\tLocalHandoff,\n\tWorkflowClaim,\n\tWorkflowKind,\n\tWorkflowStatus,\n} from \"./types.js\";\n"]}
@@ -0,0 +1,42 @@
1
+ import { type PostgresStore } from "@asm-agent/postgres";
2
+ import type { CrawlPolicy, KnowledgeSourceKind } from "./types.js";
3
+ export declare class GovernedKnowledgeService {
4
+ private readonly database;
5
+ private readonly memory;
6
+ constructor(database: PostgresStore);
7
+ registerSource(input: {
8
+ id: string;
9
+ ownerId: string;
10
+ projectId: string;
11
+ kind: KnowledgeSourceKind;
12
+ uri: string;
13
+ crawlPolicy?: CrawlPolicy;
14
+ }): Promise<void>;
15
+ approveSource(id: string, ownerId: string, projectId: string, approvedBy: string): Promise<boolean>;
16
+ stage(input: {
17
+ id: string;
18
+ sourceId: string;
19
+ ownerId: string;
20
+ projectId: string;
21
+ sourceUri: string;
22
+ content: string;
23
+ metadata?: Record<string, unknown>;
24
+ }): Promise<{
25
+ digest: string;
26
+ }>;
27
+ moderate(input: {
28
+ itemId: string;
29
+ ownerId: string;
30
+ projectId: string;
31
+ content: string;
32
+ accept: boolean;
33
+ reason: string;
34
+ }): Promise<{
35
+ status: "accepted" | "rejected";
36
+ evidenceId?: string;
37
+ memoryId?: string;
38
+ }>;
39
+ revokeItem(itemId: string, ownerId: string, projectId: string, reason: string): Promise<boolean>;
40
+ private updateModeration;
41
+ }
42
+ //# sourceMappingURL=knowledge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge.d.ts","sourceRoot":"","sources":["../src/knowledge.ts"],"names":[],"mappings":"AACA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEnE,qBAAa,wBAAwB;IAGxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAFrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAE7C,YAA6B,QAAQ,EAAE,aAAa,EAEnD;IAEK,cAAc,CAAC,KAAK,EAAE;QAC3B,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,mBAAmB,CAAC;QAC1B,GAAG,EAAE,MAAM,CAAC;QACZ,WAAW,CAAC,EAAE,WAAW,CAAC;KAC1B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;IAEK,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOxG;IAEK,KAAK,CAAC,KAAK,EAAE;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACnC,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAkB9B;IAEK,QAAQ,CAAC,KAAK,EAAE;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,UAAU,GAAG,UAAU,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CA4CvF;IAEK,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAcrG;YAEa,gBAAgB;CAM9B","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { GroundedMemoryStore, type PostgresStore } from \"@asm-agent/postgres\";\nimport type { CrawlPolicy, KnowledgeSourceKind } from \"./types.js\";\n\nexport class GovernedKnowledgeService {\n\tprivate readonly memory: GroundedMemoryStore;\n\n\tconstructor(private readonly database: PostgresStore) {\n\t\tthis.memory = new GroundedMemoryStore(database);\n\t}\n\n\tasync registerSource(input: {\n\t\tid: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tkind: KnowledgeSourceKind;\n\t\turi: string;\n\t\tcrawlPolicy?: CrawlPolicy;\n\t}): Promise<void> {\n\t\tconst parsed = validateSource(input.kind, input.uri, input.crawlPolicy);\n\t\tawait this.database.pool.query(\n\t\t\t`INSERT INTO asm_agent.knowledge_sources\n\t\t\t (id,owner_id,project_id,kind,uri,host,status,crawl_policy) VALUES ($1,$2,$3,$4,$5,$6,'pending_approval',$7::jsonb)`,\n\t\t\t[input.id, input.ownerId, input.projectId, input.kind, input.uri, parsed.host, JSON.stringify(parsed.policy)],\n\t\t);\n\t}\n\n\tasync approveSource(id: string, ownerId: string, projectId: string, approvedBy: string): Promise<boolean> {\n\t\tconst result = await this.database.pool.query(\n\t\t\t`UPDATE asm_agent.knowledge_sources SET status='approved',approved_by=$4,approved_at=clock_timestamp()\n\t\t\t WHERE id=$1 AND owner_id=$2 AND project_id=$3 AND status='pending_approval'`,\n\t\t\t[id, ownerId, projectId, approvedBy],\n\t\t);\n\t\treturn (result.rowCount ?? 0) === 1;\n\t}\n\n\tasync stage(input: {\n\t\tid: string;\n\t\tsourceId: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tsourceUri: string;\n\t\tcontent: string;\n\t\tmetadata?: Record<string, unknown>;\n\t}): Promise<{ digest: string }> {\n\t\tconst content = input.content.trim();\n\t\tif (!content) throw new Error(\"Ingestion content must not be empty\");\n\t\tconst source = await this.database.pool.query<{ uri: string; host: string | null; crawl_policy: CrawlPolicy }>(\n\t\t\t`SELECT uri,host,crawl_policy FROM asm_agent.knowledge_sources\n\t\t\t WHERE id=$1 AND owner_id=$2 AND project_id=$3 AND status='approved'`,\n\t\t\t[input.sourceId, input.ownerId, input.projectId],\n\t\t);\n\t\tconst row = source.rows[0];\n\t\tif (!row) throw new Error(\"Knowledge source is not approved in this project\");\n\t\tassertCrawlTarget(row, input.sourceUri);\n\t\tconst digest = createHash(\"sha256\").update(content).digest(\"hex\");\n\t\tawait this.database.pool.query(\n\t\t\t`INSERT INTO asm_agent.ingestion_items\n\t\t\t (id,source_id,source_uri,content_digest,status,metadata) VALUES ($1,$2,$3,$4,'pending_moderation',$5::jsonb)`,\n\t\t\t[input.id, input.sourceId, input.sourceUri, digest, JSON.stringify(input.metadata ?? {})],\n\t\t);\n\t\treturn { digest };\n\t}\n\n\tasync moderate(input: {\n\t\titemId: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tcontent: string;\n\t\taccept: boolean;\n\t\treason: string;\n\t}): Promise<{ status: \"accepted\" | \"rejected\"; evidenceId?: string; memoryId?: string }> {\n\t\tconst item = await this.database.pool.query<{ source_uri: string; content_digest: string }>(\n\t\t\t`SELECT item.source_uri,item.content_digest FROM asm_agent.ingestion_items item\n\t\t\t JOIN asm_agent.knowledge_sources source ON source.id=item.source_id\n\t\t\t WHERE item.id=$1 AND source.owner_id=$2 AND source.project_id=$3 AND item.status='pending_moderation'`,\n\t\t\t[input.itemId, input.ownerId, input.projectId],\n\t\t);\n\t\tconst row = item.rows[0];\n\t\tif (!row) throw new Error(\"Pending ingestion item not found in project\");\n\t\tif (createHash(\"sha256\").update(input.content.trim()).digest(\"hex\") !== row.content_digest)\n\t\t\tthrow new Error(\"Moderated content does not match staged digest\");\n\t\tif (!input.accept) {\n\t\t\tawait this.updateModeration(input.itemId, \"rejected\", input.reason);\n\t\t\treturn { status: \"rejected\" };\n\t\t}\n\t\tconst requestedEvidenceId = randomUUID();\n\t\tconst memoryId = randomUUID();\n\t\tconst evidence = await this.memory.createEvidence({\n\t\t\tid: requestedEvidenceId,\n\t\t\townerId: input.ownerId,\n\t\t\tprojectId: input.projectId,\n\t\t\tsourceType: \"document\",\n\t\t\tsourceUri: row.source_uri,\n\t\t\tcontentType: \"text/plain\",\n\t\t\tcontent: input.content,\n\t\t\tcapturedAt: new Date(),\n\t\t\tmetadata: { ingestionItemId: input.itemId },\n\t\t});\n\t\tawait this.memory.createMemory({\n\t\t\tid: memoryId,\n\t\t\townerId: input.ownerId,\n\t\t\tprojectId: input.projectId,\n\t\t\tkind: \"fact\",\n\t\t\tcontent: input.content,\n\t\t\tevidenceIds: [evidence.id],\n\t\t\tscopes: [{ type: \"owner\", id: input.ownerId }],\n\t\t\tmetadata: { ingestionItemId: input.itemId },\n\t\t});\n\t\tawait this.database.pool.query(\n\t\t\t`UPDATE asm_agent.ingestion_items SET status='accepted',evidence_id=$2,memory_id=$3,\n\t\t\t moderation_reason=$4,moderated_at=clock_timestamp() WHERE id=$1`,\n\t\t\t[input.itemId, evidence.id, memoryId, input.reason],\n\t\t);\n\t\treturn { status: \"accepted\", evidenceId: evidence.id, memoryId };\n\t}\n\n\tasync revokeItem(itemId: string, ownerId: string, projectId: string, reason: string): Promise<boolean> {\n\t\tconst item = await this.database.pool.query<{ memory_id: string | null }>(\n\t\t\t`SELECT item.memory_id FROM asm_agent.ingestion_items item JOIN asm_agent.knowledge_sources source ON source.id=item.source_id\n\t\t\t WHERE item.id=$1 AND source.owner_id=$2 AND source.project_id=$3 AND item.status='accepted'`,\n\t\t\t[itemId, ownerId, projectId],\n\t\t);\n\t\tconst row = item.rows[0];\n\t\tif (!row?.memory_id) return false;\n\t\tawait this.memory.revokeMemory(row.memory_id, ownerId, projectId, reason);\n\t\tawait this.database.pool.query(\n\t\t\t\"UPDATE asm_agent.ingestion_items SET status='revoked',moderation_reason=$2 WHERE id=$1\",\n\t\t\t[itemId, reason],\n\t\t);\n\t\treturn true;\n\t}\n\n\tprivate async updateModeration(itemId: string, status: \"rejected\", reason: string): Promise<void> {\n\t\tawait this.database.pool.query(\n\t\t\t\"UPDATE asm_agent.ingestion_items SET status=$2,moderation_reason=$3,moderated_at=clock_timestamp() WHERE id=$1\",\n\t\t\t[itemId, status, reason],\n\t\t);\n\t}\n}\n\nfunction validateSource(\n\tkind: KnowledgeSourceKind,\n\turi: string,\n\tpolicy?: CrawlPolicy,\n): { host: string | null; policy: CrawlPolicy | Record<string, never> } {\n\tif (kind === \"document\") return { host: null, policy: {} };\n\tconst parsed = new URL(uri);\n\tif (parsed.protocol !== \"https:\") throw new Error(\"Remote knowledge sources require HTTPS\");\n\tif (!policy || policy.respectRobotsTxt !== true) throw new Error(\"Crawl policy must respect robots.txt\");\n\tif (!Number.isInteger(policy.maxDepth) || policy.maxDepth < 0 || policy.maxDepth > 5)\n\t\tthrow new Error(\"Crawl maxDepth must be between 0 and 5\");\n\tif (!Number.isInteger(policy.maxPages) || policy.maxPages < 1 || policy.maxPages > 1_000)\n\t\tthrow new Error(\"Crawl maxPages must be between 1 and 1000\");\n\tif (!policy.allowedHosts.includes(parsed.hostname)) throw new Error(\"Source host must be explicitly allowed\");\n\treturn { host: parsed.hostname, policy };\n}\n\nfunction assertCrawlTarget(\n\tsource: { uri: string; host: string | null; crawl_policy: CrawlPolicy },\n\ttarget: string,\n): void {\n\tif (!source.host) return;\n\tconst parsed = new URL(target, source.uri);\n\tif (parsed.protocol !== \"https:\" || !source.crawl_policy.allowedHosts.includes(parsed.hostname))\n\t\tthrow new Error(\"Crawl target is outside the approved host allowlist\");\n}\n"]}
@@ -0,0 +1,112 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { GroundedMemoryStore } from "@asm-agent/postgres";
3
+ export class GovernedKnowledgeService {
4
+ database;
5
+ memory;
6
+ constructor(database) {
7
+ this.database = database;
8
+ this.memory = new GroundedMemoryStore(database);
9
+ }
10
+ async registerSource(input) {
11
+ const parsed = validateSource(input.kind, input.uri, input.crawlPolicy);
12
+ await this.database.pool.query(`INSERT INTO asm_agent.knowledge_sources
13
+ (id,owner_id,project_id,kind,uri,host,status,crawl_policy) VALUES ($1,$2,$3,$4,$5,$6,'pending_approval',$7::jsonb)`, [input.id, input.ownerId, input.projectId, input.kind, input.uri, parsed.host, JSON.stringify(parsed.policy)]);
14
+ }
15
+ async approveSource(id, ownerId, projectId, approvedBy) {
16
+ const result = await this.database.pool.query(`UPDATE asm_agent.knowledge_sources SET status='approved',approved_by=$4,approved_at=clock_timestamp()
17
+ WHERE id=$1 AND owner_id=$2 AND project_id=$3 AND status='pending_approval'`, [id, ownerId, projectId, approvedBy]);
18
+ return (result.rowCount ?? 0) === 1;
19
+ }
20
+ async stage(input) {
21
+ const content = input.content.trim();
22
+ if (!content)
23
+ throw new Error("Ingestion content must not be empty");
24
+ const source = await this.database.pool.query(`SELECT uri,host,crawl_policy FROM asm_agent.knowledge_sources
25
+ WHERE id=$1 AND owner_id=$2 AND project_id=$3 AND status='approved'`, [input.sourceId, input.ownerId, input.projectId]);
26
+ const row = source.rows[0];
27
+ if (!row)
28
+ throw new Error("Knowledge source is not approved in this project");
29
+ assertCrawlTarget(row, input.sourceUri);
30
+ const digest = createHash("sha256").update(content).digest("hex");
31
+ await this.database.pool.query(`INSERT INTO asm_agent.ingestion_items
32
+ (id,source_id,source_uri,content_digest,status,metadata) VALUES ($1,$2,$3,$4,'pending_moderation',$5::jsonb)`, [input.id, input.sourceId, input.sourceUri, digest, JSON.stringify(input.metadata ?? {})]);
33
+ return { digest };
34
+ }
35
+ async moderate(input) {
36
+ const item = await this.database.pool.query(`SELECT item.source_uri,item.content_digest FROM asm_agent.ingestion_items item
37
+ JOIN asm_agent.knowledge_sources source ON source.id=item.source_id
38
+ WHERE item.id=$1 AND source.owner_id=$2 AND source.project_id=$3 AND item.status='pending_moderation'`, [input.itemId, input.ownerId, input.projectId]);
39
+ const row = item.rows[0];
40
+ if (!row)
41
+ throw new Error("Pending ingestion item not found in project");
42
+ if (createHash("sha256").update(input.content.trim()).digest("hex") !== row.content_digest)
43
+ throw new Error("Moderated content does not match staged digest");
44
+ if (!input.accept) {
45
+ await this.updateModeration(input.itemId, "rejected", input.reason);
46
+ return { status: "rejected" };
47
+ }
48
+ const requestedEvidenceId = randomUUID();
49
+ const memoryId = randomUUID();
50
+ const evidence = await this.memory.createEvidence({
51
+ id: requestedEvidenceId,
52
+ ownerId: input.ownerId,
53
+ projectId: input.projectId,
54
+ sourceType: "document",
55
+ sourceUri: row.source_uri,
56
+ contentType: "text/plain",
57
+ content: input.content,
58
+ capturedAt: new Date(),
59
+ metadata: { ingestionItemId: input.itemId },
60
+ });
61
+ await this.memory.createMemory({
62
+ id: memoryId,
63
+ ownerId: input.ownerId,
64
+ projectId: input.projectId,
65
+ kind: "fact",
66
+ content: input.content,
67
+ evidenceIds: [evidence.id],
68
+ scopes: [{ type: "owner", id: input.ownerId }],
69
+ metadata: { ingestionItemId: input.itemId },
70
+ });
71
+ await this.database.pool.query(`UPDATE asm_agent.ingestion_items SET status='accepted',evidence_id=$2,memory_id=$3,
72
+ moderation_reason=$4,moderated_at=clock_timestamp() WHERE id=$1`, [input.itemId, evidence.id, memoryId, input.reason]);
73
+ return { status: "accepted", evidenceId: evidence.id, memoryId };
74
+ }
75
+ async revokeItem(itemId, ownerId, projectId, reason) {
76
+ const item = await this.database.pool.query(`SELECT item.memory_id FROM asm_agent.ingestion_items item JOIN asm_agent.knowledge_sources source ON source.id=item.source_id
77
+ WHERE item.id=$1 AND source.owner_id=$2 AND source.project_id=$3 AND item.status='accepted'`, [itemId, ownerId, projectId]);
78
+ const row = item.rows[0];
79
+ if (!row?.memory_id)
80
+ return false;
81
+ await this.memory.revokeMemory(row.memory_id, ownerId, projectId, reason);
82
+ await this.database.pool.query("UPDATE asm_agent.ingestion_items SET status='revoked',moderation_reason=$2 WHERE id=$1", [itemId, reason]);
83
+ return true;
84
+ }
85
+ async updateModeration(itemId, status, reason) {
86
+ await this.database.pool.query("UPDATE asm_agent.ingestion_items SET status=$2,moderation_reason=$3,moderated_at=clock_timestamp() WHERE id=$1", [itemId, status, reason]);
87
+ }
88
+ }
89
+ function validateSource(kind, uri, policy) {
90
+ if (kind === "document")
91
+ return { host: null, policy: {} };
92
+ const parsed = new URL(uri);
93
+ if (parsed.protocol !== "https:")
94
+ throw new Error("Remote knowledge sources require HTTPS");
95
+ if (!policy || policy.respectRobotsTxt !== true)
96
+ throw new Error("Crawl policy must respect robots.txt");
97
+ if (!Number.isInteger(policy.maxDepth) || policy.maxDepth < 0 || policy.maxDepth > 5)
98
+ throw new Error("Crawl maxDepth must be between 0 and 5");
99
+ if (!Number.isInteger(policy.maxPages) || policy.maxPages < 1 || policy.maxPages > 1_000)
100
+ throw new Error("Crawl maxPages must be between 1 and 1000");
101
+ if (!policy.allowedHosts.includes(parsed.hostname))
102
+ throw new Error("Source host must be explicitly allowed");
103
+ return { host: parsed.hostname, policy };
104
+ }
105
+ function assertCrawlTarget(source, target) {
106
+ if (!source.host)
107
+ return;
108
+ const parsed = new URL(target, source.uri);
109
+ if (parsed.protocol !== "https:" || !source.crawl_policy.allowedHosts.includes(parsed.hostname))
110
+ throw new Error("Crawl target is outside the approved host allowlist");
111
+ }
112
+ //# sourceMappingURL=knowledge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge.js","sourceRoot":"","sources":["../src/knowledge.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { GroundedMemoryStore, type PostgresStore } from \"@asm-agent/postgres\";\nimport type { CrawlPolicy, KnowledgeSourceKind } from \"./types.js\";\n\nexport class GovernedKnowledgeService {\n\tprivate readonly memory: GroundedMemoryStore;\n\n\tconstructor(private readonly database: PostgresStore) {\n\t\tthis.memory = new GroundedMemoryStore(database);\n\t}\n\n\tasync registerSource(input: {\n\t\tid: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tkind: KnowledgeSourceKind;\n\t\turi: string;\n\t\tcrawlPolicy?: CrawlPolicy;\n\t}): Promise<void> {\n\t\tconst parsed = validateSource(input.kind, input.uri, input.crawlPolicy);\n\t\tawait this.database.pool.query(\n\t\t\t`INSERT INTO asm_agent.knowledge_sources\n\t\t\t (id,owner_id,project_id,kind,uri,host,status,crawl_policy) VALUES ($1,$2,$3,$4,$5,$6,'pending_approval',$7::jsonb)`,\n\t\t\t[input.id, input.ownerId, input.projectId, input.kind, input.uri, parsed.host, JSON.stringify(parsed.policy)],\n\t\t);\n\t}\n\n\tasync approveSource(id: string, ownerId: string, projectId: string, approvedBy: string): Promise<boolean> {\n\t\tconst result = await this.database.pool.query(\n\t\t\t`UPDATE asm_agent.knowledge_sources SET status='approved',approved_by=$4,approved_at=clock_timestamp()\n\t\t\t WHERE id=$1 AND owner_id=$2 AND project_id=$3 AND status='pending_approval'`,\n\t\t\t[id, ownerId, projectId, approvedBy],\n\t\t);\n\t\treturn (result.rowCount ?? 0) === 1;\n\t}\n\n\tasync stage(input: {\n\t\tid: string;\n\t\tsourceId: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tsourceUri: string;\n\t\tcontent: string;\n\t\tmetadata?: Record<string, unknown>;\n\t}): Promise<{ digest: string }> {\n\t\tconst content = input.content.trim();\n\t\tif (!content) throw new Error(\"Ingestion content must not be empty\");\n\t\tconst source = await this.database.pool.query<{ uri: string; host: string | null; crawl_policy: CrawlPolicy }>(\n\t\t\t`SELECT uri,host,crawl_policy FROM asm_agent.knowledge_sources\n\t\t\t WHERE id=$1 AND owner_id=$2 AND project_id=$3 AND status='approved'`,\n\t\t\t[input.sourceId, input.ownerId, input.projectId],\n\t\t);\n\t\tconst row = source.rows[0];\n\t\tif (!row) throw new Error(\"Knowledge source is not approved in this project\");\n\t\tassertCrawlTarget(row, input.sourceUri);\n\t\tconst digest = createHash(\"sha256\").update(content).digest(\"hex\");\n\t\tawait this.database.pool.query(\n\t\t\t`INSERT INTO asm_agent.ingestion_items\n\t\t\t (id,source_id,source_uri,content_digest,status,metadata) VALUES ($1,$2,$3,$4,'pending_moderation',$5::jsonb)`,\n\t\t\t[input.id, input.sourceId, input.sourceUri, digest, JSON.stringify(input.metadata ?? {})],\n\t\t);\n\t\treturn { digest };\n\t}\n\n\tasync moderate(input: {\n\t\titemId: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tcontent: string;\n\t\taccept: boolean;\n\t\treason: string;\n\t}): Promise<{ status: \"accepted\" | \"rejected\"; evidenceId?: string; memoryId?: string }> {\n\t\tconst item = await this.database.pool.query<{ source_uri: string; content_digest: string }>(\n\t\t\t`SELECT item.source_uri,item.content_digest FROM asm_agent.ingestion_items item\n\t\t\t JOIN asm_agent.knowledge_sources source ON source.id=item.source_id\n\t\t\t WHERE item.id=$1 AND source.owner_id=$2 AND source.project_id=$3 AND item.status='pending_moderation'`,\n\t\t\t[input.itemId, input.ownerId, input.projectId],\n\t\t);\n\t\tconst row = item.rows[0];\n\t\tif (!row) throw new Error(\"Pending ingestion item not found in project\");\n\t\tif (createHash(\"sha256\").update(input.content.trim()).digest(\"hex\") !== row.content_digest)\n\t\t\tthrow new Error(\"Moderated content does not match staged digest\");\n\t\tif (!input.accept) {\n\t\t\tawait this.updateModeration(input.itemId, \"rejected\", input.reason);\n\t\t\treturn { status: \"rejected\" };\n\t\t}\n\t\tconst requestedEvidenceId = randomUUID();\n\t\tconst memoryId = randomUUID();\n\t\tconst evidence = await this.memory.createEvidence({\n\t\t\tid: requestedEvidenceId,\n\t\t\townerId: input.ownerId,\n\t\t\tprojectId: input.projectId,\n\t\t\tsourceType: \"document\",\n\t\t\tsourceUri: row.source_uri,\n\t\t\tcontentType: \"text/plain\",\n\t\t\tcontent: input.content,\n\t\t\tcapturedAt: new Date(),\n\t\t\tmetadata: { ingestionItemId: input.itemId },\n\t\t});\n\t\tawait this.memory.createMemory({\n\t\t\tid: memoryId,\n\t\t\townerId: input.ownerId,\n\t\t\tprojectId: input.projectId,\n\t\t\tkind: \"fact\",\n\t\t\tcontent: input.content,\n\t\t\tevidenceIds: [evidence.id],\n\t\t\tscopes: [{ type: \"owner\", id: input.ownerId }],\n\t\t\tmetadata: { ingestionItemId: input.itemId },\n\t\t});\n\t\tawait this.database.pool.query(\n\t\t\t`UPDATE asm_agent.ingestion_items SET status='accepted',evidence_id=$2,memory_id=$3,\n\t\t\t moderation_reason=$4,moderated_at=clock_timestamp() WHERE id=$1`,\n\t\t\t[input.itemId, evidence.id, memoryId, input.reason],\n\t\t);\n\t\treturn { status: \"accepted\", evidenceId: evidence.id, memoryId };\n\t}\n\n\tasync revokeItem(itemId: string, ownerId: string, projectId: string, reason: string): Promise<boolean> {\n\t\tconst item = await this.database.pool.query<{ memory_id: string | null }>(\n\t\t\t`SELECT item.memory_id FROM asm_agent.ingestion_items item JOIN asm_agent.knowledge_sources source ON source.id=item.source_id\n\t\t\t WHERE item.id=$1 AND source.owner_id=$2 AND source.project_id=$3 AND item.status='accepted'`,\n\t\t\t[itemId, ownerId, projectId],\n\t\t);\n\t\tconst row = item.rows[0];\n\t\tif (!row?.memory_id) return false;\n\t\tawait this.memory.revokeMemory(row.memory_id, ownerId, projectId, reason);\n\t\tawait this.database.pool.query(\n\t\t\t\"UPDATE asm_agent.ingestion_items SET status='revoked',moderation_reason=$2 WHERE id=$1\",\n\t\t\t[itemId, reason],\n\t\t);\n\t\treturn true;\n\t}\n\n\tprivate async updateModeration(itemId: string, status: \"rejected\", reason: string): Promise<void> {\n\t\tawait this.database.pool.query(\n\t\t\t\"UPDATE asm_agent.ingestion_items SET status=$2,moderation_reason=$3,moderated_at=clock_timestamp() WHERE id=$1\",\n\t\t\t[itemId, status, reason],\n\t\t);\n\t}\n}\n\nfunction validateSource(\n\tkind: KnowledgeSourceKind,\n\turi: string,\n\tpolicy?: CrawlPolicy,\n): { host: string | null; policy: CrawlPolicy | Record<string, never> } {\n\tif (kind === \"document\") return { host: null, policy: {} };\n\tconst parsed = new URL(uri);\n\tif (parsed.protocol !== \"https:\") throw new Error(\"Remote knowledge sources require HTTPS\");\n\tif (!policy || policy.respectRobotsTxt !== true) throw new Error(\"Crawl policy must respect robots.txt\");\n\tif (!Number.isInteger(policy.maxDepth) || policy.maxDepth < 0 || policy.maxDepth > 5)\n\t\tthrow new Error(\"Crawl maxDepth must be between 0 and 5\");\n\tif (!Number.isInteger(policy.maxPages) || policy.maxPages < 1 || policy.maxPages > 1_000)\n\t\tthrow new Error(\"Crawl maxPages must be between 1 and 1000\");\n\tif (!policy.allowedHosts.includes(parsed.hostname)) throw new Error(\"Source host must be explicitly allowed\");\n\treturn { host: parsed.hostname, policy };\n}\n\nfunction assertCrawlTarget(\n\tsource: { uri: string; host: string | null; crawl_policy: CrawlPolicy },\n\ttarget: string,\n): void {\n\tif (!source.host) return;\n\tconst parsed = new URL(target, source.uri);\n\tif (parsed.protocol !== \"https:\" || !source.crawl_policy.allowedHosts.includes(parsed.hostname))\n\t\tthrow new Error(\"Crawl target is outside the approved host allowlist\");\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { PostgresStore } from "@asm-agent/postgres";
2
+ import type { LocalHandoff, WorkflowClaim, WorkflowKind } from "./types.js";
3
+ export declare class WorkflowRepository {
4
+ readonly database: PostgresStore;
5
+ constructor(database: PostgresStore);
6
+ create(input: {
7
+ id: string;
8
+ ownerId: string;
9
+ projectId: string;
10
+ sessionId?: string;
11
+ kind: WorkflowKind;
12
+ payload: Record<string, unknown>;
13
+ requiresApproval?: boolean;
14
+ approvalScopeHash?: string;
15
+ availableAt?: Date;
16
+ }): Promise<void>;
17
+ approve(id: string, scopeHash: string, approvedBy: string): Promise<boolean>;
18
+ claim(workerId: string, lockMs?: number): Promise<WorkflowClaim | undefined>;
19
+ finish(id: string, workerId: string, fencingToken: number, result: Record<string, unknown>): Promise<boolean>;
20
+ getHandoff(id: string): Promise<LocalHandoff | undefined>;
21
+ }
22
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAqB5E,qBAAa,kBAAkB;IAClB,QAAQ,CAAC,QAAQ,EAAE,aAAa;IAA5C,YAAqB,QAAQ,EAAE,aAAa,EAAI;IAE1C,MAAM,CAAC,KAAK,EAAE;QACnB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,YAAY,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,WAAW,CAAC,EAAE,IAAI,CAAC;KACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBhB;IAEK,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAWjF;IAEK,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAwBjF;IAEK,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAWlH;IAEK,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAO9D;CACD","sourcesContent":["import type { PostgresStore } from \"@asm-agent/postgres\";\nimport type { PoolClient, QueryResultRow } from \"pg\";\nimport type { LocalHandoff, WorkflowClaim, WorkflowKind } from \"./types.js\";\n\ninterface WorkflowRow extends QueryResultRow {\n\tid: string;\n\tkind: WorkflowKind;\n\tinput: Record<string, unknown>;\n\tfencing_token: string;\n\tattempts: number;\n}\n\ninterface HandoffRow extends QueryResultRow {\n\tid: string;\n\tproject_id: string;\n\tfrom_session_id: string;\n\tto_session_id: string;\n\tfrom_agent_id: string;\n\tto_agent_id: string;\n\tobjective: string;\n\tstatus: LocalHandoff[\"status\"];\n}\n\nexport class WorkflowRepository {\n\tconstructor(readonly database: PostgresStore) {}\n\n\tasync create(input: {\n\t\tid: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tsessionId?: string;\n\t\tkind: WorkflowKind;\n\t\tpayload: Record<string, unknown>;\n\t\trequiresApproval?: boolean;\n\t\tapprovalScopeHash?: string;\n\t\tavailableAt?: Date;\n\t}): Promise<void> {\n\t\tif (input.requiresApproval && !input.approvalScopeHash)\n\t\t\tthrow new Error(\"Approval-bound workflow requires scope hash\");\n\t\tawait this.database.transaction(async (client) => {\n\t\t\tawait client.query(\n\t\t\t\t`INSERT INTO asm_agent.durable_workflows\n\t\t\t\t (id,owner_id,project_id,session_id,kind,status,input,approval_scope_hash,available_at)\n\t\t\t\t VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9)`,\n\t\t\t\t[\n\t\t\t\t\tinput.id,\n\t\t\t\t\tinput.ownerId,\n\t\t\t\t\tinput.projectId,\n\t\t\t\t\tinput.sessionId ?? null,\n\t\t\t\t\tinput.kind,\n\t\t\t\t\tinput.requiresApproval ? \"waiting_approval\" : \"pending\",\n\t\t\t\t\tJSON.stringify(input.payload),\n\t\t\t\t\tinput.approvalScopeHash ?? null,\n\t\t\t\t\tinput.availableAt ?? new Date(),\n\t\t\t\t],\n\t\t\t);\n\t\t\tawait appendEvent(client, input.id, \"created\", { kind: input.kind });\n\t\t});\n\t}\n\n\tasync approve(id: string, scopeHash: string, approvedBy: string): Promise<boolean> {\n\t\treturn this.database.transaction(async (client) => {\n\t\t\tconst result = await client.query(\n\t\t\t\t`UPDATE asm_agent.durable_workflows SET status='pending', approved_by=$3, approved_at=clock_timestamp()\n\t\t\t\t WHERE id=$1 AND status='waiting_approval' AND approval_scope_hash=$2`,\n\t\t\t\t[id, scopeHash, approvedBy],\n\t\t\t);\n\t\t\tif ((result.rowCount ?? 0) !== 1) return false;\n\t\t\tawait appendEvent(client, id, \"approved\", { approvedBy });\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tasync claim(workerId: string, lockMs = 30_000): Promise<WorkflowClaim | undefined> {\n\t\treturn this.database.transaction(async (client) => {\n\t\t\tconst result = await client.query<WorkflowRow>(\n\t\t\t\t`WITH candidate AS (\n\t\t\t\t SELECT id FROM asm_agent.durable_workflows WHERE status IN ('pending','running')\n\t\t\t\t AND available_at <= clock_timestamp() AND (locked_until IS NULL OR locked_until <= clock_timestamp())\n\t\t\t\t ORDER BY available_at,id FOR UPDATE SKIP LOCKED LIMIT 1)\n\t\t\t\t UPDATE asm_agent.durable_workflows workflow SET status='running', locked_by=$1,\n\t\t\t\t locked_until=clock_timestamp()+($2*interval '1 millisecond'),\n\t\t\t\t fencing_token=fencing_token+1, attempts=attempts+1 FROM candidate\n\t\t\t\t WHERE workflow.id=candidate.id RETURNING workflow.*`,\n\t\t\t\t[workerId, lockMs],\n\t\t\t);\n\t\t\tconst row = result.rows[0];\n\t\t\tif (!row) return undefined;\n\t\t\tawait appendEvent(client, row.id, \"claimed\", { workerId, fencingToken: Number(row.fencing_token) });\n\t\t\treturn {\n\t\t\t\tid: row.id,\n\t\t\t\tkind: row.kind,\n\t\t\t\tinput: row.input,\n\t\t\t\tfencingToken: Number(row.fencing_token),\n\t\t\t\tattempts: row.attempts,\n\t\t\t};\n\t\t});\n\t}\n\n\tasync finish(id: string, workerId: string, fencingToken: number, result: Record<string, unknown>): Promise<boolean> {\n\t\treturn this.database.transaction(async (client) => {\n\t\t\tconst updated = await client.query(\n\t\t\t\t`UPDATE asm_agent.durable_workflows SET status='completed',result=$4::jsonb,completed_at=clock_timestamp(),\n\t\t\t\t locked_by=NULL,locked_until=NULL WHERE id=$1 AND locked_by=$2 AND fencing_token=$3 AND status='running'`,\n\t\t\t\t[id, workerId, fencingToken, JSON.stringify(result)],\n\t\t\t);\n\t\t\tif ((updated.rowCount ?? 0) !== 1) return false;\n\t\t\tawait appendEvent(client, id, \"completed\", result);\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tasync getHandoff(id: string): Promise<LocalHandoff | undefined> {\n\t\tconst result = await this.database.pool.query<HandoffRow>(\n\t\t\t\"SELECT * FROM asm_agent.local_agent_handoffs WHERE id=$1\",\n\t\t\t[id],\n\t\t);\n\t\tconst row = result.rows[0];\n\t\treturn row ? toHandoff(row) : undefined;\n\t}\n}\n\nasync function appendEvent(\n\tclient: PoolClient,\n\tworkflowId: string,\n\teventType: string,\n\tpayload: Record<string, unknown>,\n): Promise<void> {\n\tawait client.query(\n\t\t\"INSERT INTO asm_agent.workflow_events (workflow_id,event_type,payload) VALUES ($1,$2,$3::jsonb)\",\n\t\t[workflowId, eventType, JSON.stringify(payload)],\n\t);\n}\n\nfunction toHandoff(row: HandoffRow): LocalHandoff {\n\treturn {\n\t\tid: row.id,\n\t\tprojectId: row.project_id,\n\t\tfromSessionId: row.from_session_id,\n\t\ttoSessionId: row.to_session_id,\n\t\tfromAgentId: row.from_agent_id,\n\t\ttoAgentId: row.to_agent_id,\n\t\tobjective: row.objective,\n\t\tstatus: row.status,\n\t};\n}\n"]}
@@ -0,0 +1,90 @@
1
+ export class WorkflowRepository {
2
+ database;
3
+ constructor(database) {
4
+ this.database = database;
5
+ }
6
+ async create(input) {
7
+ if (input.requiresApproval && !input.approvalScopeHash)
8
+ throw new Error("Approval-bound workflow requires scope hash");
9
+ await this.database.transaction(async (client) => {
10
+ await client.query(`INSERT INTO asm_agent.durable_workflows
11
+ (id,owner_id,project_id,session_id,kind,status,input,approval_scope_hash,available_at)
12
+ VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9)`, [
13
+ input.id,
14
+ input.ownerId,
15
+ input.projectId,
16
+ input.sessionId ?? null,
17
+ input.kind,
18
+ input.requiresApproval ? "waiting_approval" : "pending",
19
+ JSON.stringify(input.payload),
20
+ input.approvalScopeHash ?? null,
21
+ input.availableAt ?? new Date(),
22
+ ]);
23
+ await appendEvent(client, input.id, "created", { kind: input.kind });
24
+ });
25
+ }
26
+ async approve(id, scopeHash, approvedBy) {
27
+ return this.database.transaction(async (client) => {
28
+ const result = await client.query(`UPDATE asm_agent.durable_workflows SET status='pending', approved_by=$3, approved_at=clock_timestamp()
29
+ WHERE id=$1 AND status='waiting_approval' AND approval_scope_hash=$2`, [id, scopeHash, approvedBy]);
30
+ if ((result.rowCount ?? 0) !== 1)
31
+ return false;
32
+ await appendEvent(client, id, "approved", { approvedBy });
33
+ return true;
34
+ });
35
+ }
36
+ async claim(workerId, lockMs = 30_000) {
37
+ return this.database.transaction(async (client) => {
38
+ const result = await client.query(`WITH candidate AS (
39
+ SELECT id FROM asm_agent.durable_workflows WHERE status IN ('pending','running')
40
+ AND available_at <= clock_timestamp() AND (locked_until IS NULL OR locked_until <= clock_timestamp())
41
+ ORDER BY available_at,id FOR UPDATE SKIP LOCKED LIMIT 1)
42
+ UPDATE asm_agent.durable_workflows workflow SET status='running', locked_by=$1,
43
+ locked_until=clock_timestamp()+($2*interval '1 millisecond'),
44
+ fencing_token=fencing_token+1, attempts=attempts+1 FROM candidate
45
+ WHERE workflow.id=candidate.id RETURNING workflow.*`, [workerId, lockMs]);
46
+ const row = result.rows[0];
47
+ if (!row)
48
+ return undefined;
49
+ await appendEvent(client, row.id, "claimed", { workerId, fencingToken: Number(row.fencing_token) });
50
+ return {
51
+ id: row.id,
52
+ kind: row.kind,
53
+ input: row.input,
54
+ fencingToken: Number(row.fencing_token),
55
+ attempts: row.attempts,
56
+ };
57
+ });
58
+ }
59
+ async finish(id, workerId, fencingToken, result) {
60
+ return this.database.transaction(async (client) => {
61
+ const updated = await client.query(`UPDATE asm_agent.durable_workflows SET status='completed',result=$4::jsonb,completed_at=clock_timestamp(),
62
+ locked_by=NULL,locked_until=NULL WHERE id=$1 AND locked_by=$2 AND fencing_token=$3 AND status='running'`, [id, workerId, fencingToken, JSON.stringify(result)]);
63
+ if ((updated.rowCount ?? 0) !== 1)
64
+ return false;
65
+ await appendEvent(client, id, "completed", result);
66
+ return true;
67
+ });
68
+ }
69
+ async getHandoff(id) {
70
+ const result = await this.database.pool.query("SELECT * FROM asm_agent.local_agent_handoffs WHERE id=$1", [id]);
71
+ const row = result.rows[0];
72
+ return row ? toHandoff(row) : undefined;
73
+ }
74
+ }
75
+ async function appendEvent(client, workflowId, eventType, payload) {
76
+ await client.query("INSERT INTO asm_agent.workflow_events (workflow_id,event_type,payload) VALUES ($1,$2,$3::jsonb)", [workflowId, eventType, JSON.stringify(payload)]);
77
+ }
78
+ function toHandoff(row) {
79
+ return {
80
+ id: row.id,
81
+ projectId: row.project_id,
82
+ fromSessionId: row.from_session_id,
83
+ toSessionId: row.to_session_id,
84
+ fromAgentId: row.from_agent_id,
85
+ toAgentId: row.to_agent_id,
86
+ objective: row.objective,
87
+ status: row.status,
88
+ };
89
+ }
90
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.js","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PostgresStore } from \"@asm-agent/postgres\";\nimport type { PoolClient, QueryResultRow } from \"pg\";\nimport type { LocalHandoff, WorkflowClaim, WorkflowKind } from \"./types.js\";\n\ninterface WorkflowRow extends QueryResultRow {\n\tid: string;\n\tkind: WorkflowKind;\n\tinput: Record<string, unknown>;\n\tfencing_token: string;\n\tattempts: number;\n}\n\ninterface HandoffRow extends QueryResultRow {\n\tid: string;\n\tproject_id: string;\n\tfrom_session_id: string;\n\tto_session_id: string;\n\tfrom_agent_id: string;\n\tto_agent_id: string;\n\tobjective: string;\n\tstatus: LocalHandoff[\"status\"];\n}\n\nexport class WorkflowRepository {\n\tconstructor(readonly database: PostgresStore) {}\n\n\tasync create(input: {\n\t\tid: string;\n\t\townerId: string;\n\t\tprojectId: string;\n\t\tsessionId?: string;\n\t\tkind: WorkflowKind;\n\t\tpayload: Record<string, unknown>;\n\t\trequiresApproval?: boolean;\n\t\tapprovalScopeHash?: string;\n\t\tavailableAt?: Date;\n\t}): Promise<void> {\n\t\tif (input.requiresApproval && !input.approvalScopeHash)\n\t\t\tthrow new Error(\"Approval-bound workflow requires scope hash\");\n\t\tawait this.database.transaction(async (client) => {\n\t\t\tawait client.query(\n\t\t\t\t`INSERT INTO asm_agent.durable_workflows\n\t\t\t\t (id,owner_id,project_id,session_id,kind,status,input,approval_scope_hash,available_at)\n\t\t\t\t VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9)`,\n\t\t\t\t[\n\t\t\t\t\tinput.id,\n\t\t\t\t\tinput.ownerId,\n\t\t\t\t\tinput.projectId,\n\t\t\t\t\tinput.sessionId ?? null,\n\t\t\t\t\tinput.kind,\n\t\t\t\t\tinput.requiresApproval ? \"waiting_approval\" : \"pending\",\n\t\t\t\t\tJSON.stringify(input.payload),\n\t\t\t\t\tinput.approvalScopeHash ?? null,\n\t\t\t\t\tinput.availableAt ?? new Date(),\n\t\t\t\t],\n\t\t\t);\n\t\t\tawait appendEvent(client, input.id, \"created\", { kind: input.kind });\n\t\t});\n\t}\n\n\tasync approve(id: string, scopeHash: string, approvedBy: string): Promise<boolean> {\n\t\treturn this.database.transaction(async (client) => {\n\t\t\tconst result = await client.query(\n\t\t\t\t`UPDATE asm_agent.durable_workflows SET status='pending', approved_by=$3, approved_at=clock_timestamp()\n\t\t\t\t WHERE id=$1 AND status='waiting_approval' AND approval_scope_hash=$2`,\n\t\t\t\t[id, scopeHash, approvedBy],\n\t\t\t);\n\t\t\tif ((result.rowCount ?? 0) !== 1) return false;\n\t\t\tawait appendEvent(client, id, \"approved\", { approvedBy });\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tasync claim(workerId: string, lockMs = 30_000): Promise<WorkflowClaim | undefined> {\n\t\treturn this.database.transaction(async (client) => {\n\t\t\tconst result = await client.query<WorkflowRow>(\n\t\t\t\t`WITH candidate AS (\n\t\t\t\t SELECT id FROM asm_agent.durable_workflows WHERE status IN ('pending','running')\n\t\t\t\t AND available_at <= clock_timestamp() AND (locked_until IS NULL OR locked_until <= clock_timestamp())\n\t\t\t\t ORDER BY available_at,id FOR UPDATE SKIP LOCKED LIMIT 1)\n\t\t\t\t UPDATE asm_agent.durable_workflows workflow SET status='running', locked_by=$1,\n\t\t\t\t locked_until=clock_timestamp()+($2*interval '1 millisecond'),\n\t\t\t\t fencing_token=fencing_token+1, attempts=attempts+1 FROM candidate\n\t\t\t\t WHERE workflow.id=candidate.id RETURNING workflow.*`,\n\t\t\t\t[workerId, lockMs],\n\t\t\t);\n\t\t\tconst row = result.rows[0];\n\t\t\tif (!row) return undefined;\n\t\t\tawait appendEvent(client, row.id, \"claimed\", { workerId, fencingToken: Number(row.fencing_token) });\n\t\t\treturn {\n\t\t\t\tid: row.id,\n\t\t\t\tkind: row.kind,\n\t\t\t\tinput: row.input,\n\t\t\t\tfencingToken: Number(row.fencing_token),\n\t\t\t\tattempts: row.attempts,\n\t\t\t};\n\t\t});\n\t}\n\n\tasync finish(id: string, workerId: string, fencingToken: number, result: Record<string, unknown>): Promise<boolean> {\n\t\treturn this.database.transaction(async (client) => {\n\t\t\tconst updated = await client.query(\n\t\t\t\t`UPDATE asm_agent.durable_workflows SET status='completed',result=$4::jsonb,completed_at=clock_timestamp(),\n\t\t\t\t locked_by=NULL,locked_until=NULL WHERE id=$1 AND locked_by=$2 AND fencing_token=$3 AND status='running'`,\n\t\t\t\t[id, workerId, fencingToken, JSON.stringify(result)],\n\t\t\t);\n\t\t\tif ((updated.rowCount ?? 0) !== 1) return false;\n\t\t\tawait appendEvent(client, id, \"completed\", result);\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tasync getHandoff(id: string): Promise<LocalHandoff | undefined> {\n\t\tconst result = await this.database.pool.query<HandoffRow>(\n\t\t\t\"SELECT * FROM asm_agent.local_agent_handoffs WHERE id=$1\",\n\t\t\t[id],\n\t\t);\n\t\tconst row = result.rows[0];\n\t\treturn row ? toHandoff(row) : undefined;\n\t}\n}\n\nasync function appendEvent(\n\tclient: PoolClient,\n\tworkflowId: string,\n\teventType: string,\n\tpayload: Record<string, unknown>,\n): Promise<void> {\n\tawait client.query(\n\t\t\"INSERT INTO asm_agent.workflow_events (workflow_id,event_type,payload) VALUES ($1,$2,$3::jsonb)\",\n\t\t[workflowId, eventType, JSON.stringify(payload)],\n\t);\n}\n\nfunction toHandoff(row: HandoffRow): LocalHandoff {\n\treturn {\n\t\tid: row.id,\n\t\tprojectId: row.project_id,\n\t\tfromSessionId: row.from_session_id,\n\t\ttoSessionId: row.to_session_id,\n\t\tfromAgentId: row.from_agent_id,\n\t\ttoAgentId: row.to_agent_id,\n\t\tobjective: row.objective,\n\t\tstatus: row.status,\n\t};\n}\n"]}
@@ -0,0 +1,28 @@
1
+ export type KnowledgeSourceKind = "document" | "website" | "api";
2
+ export type KnowledgeSourceStatus = "pending_approval" | "approved" | "paused" | "revoked";
3
+ export type WorkflowKind = "ingestion" | "research" | "evaluation" | "learning";
4
+ export type WorkflowStatus = "pending" | "waiting_approval" | "running" | "completed" | "failed" | "cancelled";
5
+ export interface CrawlPolicy {
6
+ allowedHosts: string[];
7
+ maxDepth: number;
8
+ maxPages: number;
9
+ respectRobotsTxt: true;
10
+ }
11
+ export interface WorkflowClaim {
12
+ id: string;
13
+ kind: WorkflowKind;
14
+ input: Record<string, unknown>;
15
+ fencingToken: number;
16
+ attempts: number;
17
+ }
18
+ export interface LocalHandoff {
19
+ id: string;
20
+ projectId: string;
21
+ fromSessionId: string;
22
+ toSessionId: string;
23
+ fromAgentId: string;
24
+ toAgentId: string;
25
+ objective: string;
26
+ status: "offered" | "accepted" | "rejected" | "completed" | "cancelled";
27
+ }
28
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,SAAS,GAAG,KAAK,CAAC;AACjE,MAAM,MAAM,qBAAqB,GAAG,kBAAkB,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;AAC3F,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,CAAC;AAChF,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,kBAAkB,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE/G,MAAM,WAAW,WAAW;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC;CACxE","sourcesContent":["export type KnowledgeSourceKind = \"document\" | \"website\" | \"api\";\nexport type KnowledgeSourceStatus = \"pending_approval\" | \"approved\" | \"paused\" | \"revoked\";\nexport type WorkflowKind = \"ingestion\" | \"research\" | \"evaluation\" | \"learning\";\nexport type WorkflowStatus = \"pending\" | \"waiting_approval\" | \"running\" | \"completed\" | \"failed\" | \"cancelled\";\n\nexport interface CrawlPolicy {\n\tallowedHosts: string[];\n\tmaxDepth: number;\n\tmaxPages: number;\n\trespectRobotsTxt: true;\n}\n\nexport interface WorkflowClaim {\n\tid: string;\n\tkind: WorkflowKind;\n\tinput: Record<string, unknown>;\n\tfencingToken: number;\n\tattempts: number;\n}\n\nexport interface LocalHandoff {\n\tid: string;\n\tprojectId: string;\n\tfromSessionId: string;\n\ttoSessionId: string;\n\tfromAgentId: string;\n\ttoAgentId: string;\n\tobjective: string;\n\tstatus: \"offered\" | \"accepted\" | \"rejected\" | \"completed\" | \"cancelled\";\n}\n"]}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type KnowledgeSourceKind = \"document\" | \"website\" | \"api\";\nexport type KnowledgeSourceStatus = \"pending_approval\" | \"approved\" | \"paused\" | \"revoked\";\nexport type WorkflowKind = \"ingestion\" | \"research\" | \"evaluation\" | \"learning\";\nexport type WorkflowStatus = \"pending\" | \"waiting_approval\" | \"running\" | \"completed\" | \"failed\" | \"cancelled\";\n\nexport interface CrawlPolicy {\n\tallowedHosts: string[];\n\tmaxDepth: number;\n\tmaxPages: number;\n\trespectRobotsTxt: true;\n}\n\nexport interface WorkflowClaim {\n\tid: string;\n\tkind: WorkflowKind;\n\tinput: Record<string, unknown>;\n\tfencingToken: number;\n\tattempts: number;\n}\n\nexport interface LocalHandoff {\n\tid: string;\n\tprojectId: string;\n\tfromSessionId: string;\n\ttoSessionId: string;\n\tfromAgentId: string;\n\ttoAgentId: string;\n\tobjective: string;\n\tstatus: \"offered\" | \"accepted\" | \"rejected\" | \"completed\" | \"cancelled\";\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@asm-agent/workflows",
3
+ "version": "0.8.2",
4
+ "description": "Governed knowledge ingestion, durable workflows, and local agent handoff",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "clean": "shx rm -rf dist",
20
+ "build": "tsgo -p tsconfig.build.json",
21
+ "test": "vitest --run",
22
+ "prepublishOnly": "npm run clean && npm run build"
23
+ },
24
+ "dependencies": {
25
+ "@asm-agent/postgres": "^0.8.2",
26
+ "pg": "^8.16.3"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^24.3.0",
30
+ "@types/pg": "^8.15.5",
31
+ "typescript": "^7.0.2",
32
+ "vitest": "^4.1.10"
33
+ },
34
+ "engines": {
35
+ "node": ">=22.8.0"
36
+ },
37
+ "license": "(AGPL-3.0-only OR LicenseRef-ASM-Commercial)",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/AletheionAGI/asm-agent.git",
41
+ "directory": "packages/workflows"
42
+ }
43
+ }