@loomfsm/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/cc-adapter/commands/done.md +30 -0
- package/cc-adapter/commands/task.md +29 -0
- package/cc-adapter/pipeline-guard.sh +58 -0
- package/cc-adapter/pipeline-stop.sh +65 -0
- package/dist/src/bin/stdio.d.ts +2 -0
- package/dist/src/bin/stdio.js +43 -0
- package/dist/src/bin/stdio.js.map +1 -0
- package/dist/src/bootstrap.d.ts +4 -0
- package/dist/src/bootstrap.js +130 -0
- package/dist/src/bootstrap.js.map +1 -0
- package/dist/src/index.d.ts +21 -0
- package/dist/src/index.js +16 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib/parse-task-args.d.ts +4 -0
- package/dist/src/lib/parse-task-args.js +38 -0
- package/dist/src/lib/parse-task-args.js.map +1 -0
- package/dist/src/lib/persist-progress.d.ts +2 -0
- package/dist/src/lib/persist-progress.js +20 -0
- package/dist/src/lib/persist-progress.js.map +1 -0
- package/dist/src/server.d.ts +25 -0
- package/dist/src/server.js +257 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/tools/backup.d.ts +5 -0
- package/dist/src/tools/backup.js +69 -0
- package/dist/src/tools/backup.js.map +1 -0
- package/dist/src/tools/continue-task.d.ts +7 -0
- package/dist/src/tools/continue-task.js +156 -0
- package/dist/src/tools/continue-task.js.map +1 -0
- package/dist/src/tools/extensions-list.d.ts +2 -0
- package/dist/src/tools/extensions-list.js +49 -0
- package/dist/src/tools/extensions-list.js.map +1 -0
- package/dist/src/tools/issue-marker.d.ts +5 -0
- package/dist/src/tools/issue-marker.js +57 -0
- package/dist/src/tools/issue-marker.js.map +1 -0
- package/dist/src/tools/meta.d.ts +6 -0
- package/dist/src/tools/meta.js +52 -0
- package/dist/src/tools/meta.js.map +1 -0
- package/dist/src/tools/recover.d.ts +7 -0
- package/dist/src/tools/recover.js +243 -0
- package/dist/src/tools/recover.js.map +1 -0
- package/dist/src/tools/restore.d.ts +5 -0
- package/dist/src/tools/restore.js +82 -0
- package/dist/src/tools/restore.js.map +1 -0
- package/dist/src/tools/run-task.d.ts +7 -0
- package/dist/src/tools/run-task.js +172 -0
- package/dist/src/tools/run-task.js.map +1 -0
- package/dist/src/tools/state-get.d.ts +2 -0
- package/dist/src/tools/state-get.js +176 -0
- package/dist/src/tools/state-get.js.map +1 -0
- package/dist/src/transport-adapter.d.ts +6 -0
- package/dist/src/transport-adapter.js +94 -0
- package/dist/src/transport-adapter.js.map +1 -0
- package/dist/src/types.d.ts +171 -0
- package/dist/src/types.js +9 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.js +8 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// pipeline_extensions_list — read-only inspection of the
|
|
2
|
+
// installed_extensions registry. Filters narrow the result set via
|
|
3
|
+
// SQL WHERE clauses; `include_manifest` toggles whether the canonical
|
|
4
|
+
// manifest_json column is parsed back into a typed manifest object.
|
|
5
|
+
import { openDb } from "@loomfsm/kernel";
|
|
6
|
+
export function createExtensionsListTool() {
|
|
7
|
+
return async (input) => {
|
|
8
|
+
const db = openDb(input.project_dir);
|
|
9
|
+
const wheres = [];
|
|
10
|
+
const params = [];
|
|
11
|
+
if (input.kind !== undefined) {
|
|
12
|
+
wheres.push("kind = ?");
|
|
13
|
+
params.push(input.kind);
|
|
14
|
+
}
|
|
15
|
+
if (input.status !== undefined) {
|
|
16
|
+
wheres.push("status = ?");
|
|
17
|
+
params.push(input.status);
|
|
18
|
+
}
|
|
19
|
+
const whereClause = wheres.length > 0 ? ` WHERE ${wheres.join(" AND ")}` : "";
|
|
20
|
+
const rows = db
|
|
21
|
+
.prepare("SELECT id, kind, name, publisher, version, status, installed_at, updated_at, failure_reason, manifest_json " +
|
|
22
|
+
"FROM installed_extensions" +
|
|
23
|
+
whereClause +
|
|
24
|
+
" ORDER BY id")
|
|
25
|
+
.all(...params);
|
|
26
|
+
const include = input.include_manifest === true;
|
|
27
|
+
const extensions = rows.map((r) => {
|
|
28
|
+
const entry = {
|
|
29
|
+
id: r.id,
|
|
30
|
+
kind: r.kind,
|
|
31
|
+
name: r.name,
|
|
32
|
+
publisher: r.publisher,
|
|
33
|
+
version: r.version,
|
|
34
|
+
status: r.status,
|
|
35
|
+
installed_at: r.installed_at,
|
|
36
|
+
updated_at: r.updated_at,
|
|
37
|
+
};
|
|
38
|
+
if (r.failure_reason !== null) {
|
|
39
|
+
entry.failure_reason = r.failure_reason;
|
|
40
|
+
}
|
|
41
|
+
if (include) {
|
|
42
|
+
entry.manifest = JSON.parse(r.manifest_json);
|
|
43
|
+
}
|
|
44
|
+
return entry;
|
|
45
|
+
});
|
|
46
|
+
return { extensions };
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=extensions-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extensions-list.js","sourceRoot":"","sources":["../../../src/tools/extensions-list.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,mEAAmE;AACnE,sEAAsE;AACtE,oEAAoE;AAEpE,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAuBzC,MAAM,UAAU,wBAAwB;IAItC,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE;QACrB,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAErC,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAwB,EAAE,CAAC;QACvC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QACD,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAE9E,MAAM,IAAI,GAAG,EAAE;aACZ,OAAO,CACN,6GAA6G;YAC3G,2BAA2B;YAC3B,WAAW;YACX,cAAc,CACjB;aACA,GAAG,CAAC,GAAG,MAAM,CAA8B,CAAC;QAE/C,MAAM,OAAO,GAAG,KAAK,CAAC,gBAAgB,KAAK,IAAI,CAAC;QAEhD,MAAM,UAAU,GAA0B,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACvD,MAAM,KAAK,GAAwB;gBACjC,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,IAAI,EAAE,CAAC,CAAC,IAAqB;gBAC7B,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,MAAM,EAAE,CAAC,CAAC,MAAuC;gBACjD,YAAY,EAAE,CAAC,CAAC,YAAY;gBAC5B,UAAU,EAAE,CAAC,CAAC,UAAU;aACzB,CAAC;YACF,IAAI,CAAC,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;gBAC9B,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC,cAAc,CAAC;YAC1C,CAAC;YACD,IAAI,OAAO,EAAE,CAAC;gBACZ,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAsB,CAAC;YACpE,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;QAEH,OAAO,EAAE,UAAU,EAAE,CAAC;IACxB,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IssueCrossOwnerMarkerInput, IssueCrossOwnerMarkerResponse, ToolHandler } from "../types.js";
|
|
2
|
+
export interface IssueMarkerDeps {
|
|
3
|
+
allowlistPath?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function createIssueCrossOwnerMarkerTool(deps?: IssueMarkerDeps): ToolHandler<IssueCrossOwnerMarkerInput, IssueCrossOwnerMarkerResponse>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// pipeline_issue_cross_owner_marker — mint a single-use, TTL-bounded
|
|
2
|
+
// cross-owner bypass marker.
|
|
3
|
+
//
|
|
4
|
+
// Composition: project-dir allowlist → mint inside one
|
|
5
|
+
// withStateTransaction (the bypass_markers row is written under the
|
|
6
|
+
// threaded NowToken; the TTL is tx.now + ttl_ms with no host-clock read)
|
|
7
|
+
// → return the full marker the caller passes verbatim to pipeline_recover.
|
|
8
|
+
//
|
|
9
|
+
// Possessing the signing key (env var or user-global key file, both
|
|
10
|
+
// OUTSIDE any project dir) IS the authorization to mint — there is no
|
|
11
|
+
// owner check here. Absent a key the mint refuses with BYPASS_KEY_MISSING.
|
|
12
|
+
//
|
|
13
|
+
// Refusals (allowlist, missing key, bad TTL) are error-shaped responses;
|
|
14
|
+
// only programmer errors throw.
|
|
15
|
+
import { assertProjectDirAllowed, captureNow, issueCrossOwnerMarker, KernelError, withStateTransaction, } from "@loomfsm/kernel";
|
|
16
|
+
export function createIssueCrossOwnerMarkerTool(deps = {}) {
|
|
17
|
+
return async (input) => {
|
|
18
|
+
// 1. Project-dir allowlist.
|
|
19
|
+
try {
|
|
20
|
+
await assertProjectDirAllowed(input.project_dir, deps.allowlistPath !== undefined ? { allowlistPath: deps.allowlistPath } : undefined);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
return refusal(err);
|
|
24
|
+
}
|
|
25
|
+
// 2. Mint + persist the marker inside one tx.
|
|
26
|
+
try {
|
|
27
|
+
const marker = await withStateTransaction(input.project_dir, captureNow(), (tx) => issueCrossOwnerMarker(tx, {
|
|
28
|
+
driver_state_id: input.driver_state_id,
|
|
29
|
+
ttl_ms: input.ttl_ms,
|
|
30
|
+
}));
|
|
31
|
+
return {
|
|
32
|
+
key_id: marker.key_id,
|
|
33
|
+
hmac: marker.hmac,
|
|
34
|
+
issued_at: marker.issued_at,
|
|
35
|
+
expires_at: marker.expires_at,
|
|
36
|
+
reason: marker.reason,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return refusal(err);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function refusal(err) {
|
|
45
|
+
if (err instanceof KernelError) {
|
|
46
|
+
return {
|
|
47
|
+
key_id: null,
|
|
48
|
+
hmac: null,
|
|
49
|
+
issued_at: null,
|
|
50
|
+
expires_at: null,
|
|
51
|
+
reason: null,
|
|
52
|
+
error: { code: err.code, message: err.message },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=issue-marker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-marker.js","sourceRoot":"","sources":["../../../src/tools/issue-marker.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,6BAA6B;AAC7B,EAAE;AACF,uDAAuD;AACvD,oEAAoE;AACpE,yEAAyE;AACzE,2EAA2E;AAC3E,EAAE;AACF,oEAAoE;AACpE,sEAAsE;AACtE,2EAA2E;AAC3E,EAAE;AACF,yEAAyE;AACzE,gCAAgC;AAEhC,OAAO,EACL,uBAAuB,EACvB,UAAU,EACV,qBAAqB,EACrB,WAAW,EACX,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAYzB,MAAM,UAAU,+BAA+B,CAC7C,OAAwB,EAAE;IAE1B,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE;QACrB,4BAA4B;QAC5B,IAAI,CAAC;YACH,MAAM,uBAAuB,CAC3B,KAAK,CAAC,WAAW,EACjB,IAAI,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,SAAS,CACrF,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;QAED,8CAA8C;QAC9C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,oBAAoB,CACvC,KAAK,CAAC,WAAW,EACjB,UAAU,EAAE,EACZ,CAAC,EAAE,EAAE,EAAE,CACL,qBAAqB,CAAC,EAAE,EAAE;gBACxB,eAAe,EAAE,KAAK,CAAC,eAAe;gBACtC,MAAM,EAAE,KAAK,CAAC,MAAM;aACrB,CAAC,CACL,CAAC;YACF,OAAO;gBACL,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,UAAU,EAAE,MAAM,CAAC,UAAU;gBAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;aACtB,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,GAAY;IAC3B,IAAI,GAAG,YAAY,WAAW,EAAE,CAAC;QAC/B,OAAO;YACL,MAAM,EAAE,IAAI;YACZ,IAAI,EAAE,IAAI;YACV,SAAS,EAAE,IAAI;YACf,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,IAAI;YACZ,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE;SAChD,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MetaInput, PipelineMetaResponse, ToolHandler } from "../types.js";
|
|
2
|
+
interface MetaInputWithProject extends MetaInput {
|
|
3
|
+
project_dir: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function createMetaTool(): ToolHandler<MetaInputWithProject, PipelineMetaResponse>;
|
|
6
|
+
export type { MetaInputWithProject };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// pipeline_meta — protocol-discovery handler. Reads providers and
|
|
2
|
+
// bundles from the installed_extensions registry; advertises the
|
|
3
|
+
// host-neutral flag vocabulary derived from FLAG_TO_PRESET so the
|
|
4
|
+
// parser surface and the meta echo can never drift.
|
|
5
|
+
import { openDb } from "@loomfsm/kernel";
|
|
6
|
+
import { FLAG_TO_PRESET } from "../lib/parse-task-args.js";
|
|
7
|
+
import { KERNEL_VERSION, PLUGIN_API_VERSION, PROTOCOL_VERSION, } from "../version.js";
|
|
8
|
+
const ACTIVE_TRANSPORT = "mcp-server";
|
|
9
|
+
const AVAILABLE_TRANSPORTS = [ACTIVE_TRANSPORT];
|
|
10
|
+
const SANDBOX_KIND = "passthrough";
|
|
11
|
+
// No-API-key fallback that ships with the kernel; once the provider-
|
|
12
|
+
// router config layer wires into kernel boot, this switches to the
|
|
13
|
+
// router-resolved value.
|
|
14
|
+
const ACTIVE_DEFAULT_PROVIDER = "claude-code-shuttle";
|
|
15
|
+
export function createMetaTool() {
|
|
16
|
+
return async (input) => {
|
|
17
|
+
const db = openDb(input.project_dir);
|
|
18
|
+
const providerRows = db
|
|
19
|
+
.prepare("SELECT name, version FROM installed_extensions " +
|
|
20
|
+
"WHERE kind = 'provider' AND status = 'enabled' " +
|
|
21
|
+
"ORDER BY name")
|
|
22
|
+
.all();
|
|
23
|
+
const bundleRows = db
|
|
24
|
+
.prepare("SELECT name, version FROM installed_extensions " +
|
|
25
|
+
"WHERE kind = 'bundle' AND status = 'enabled' " +
|
|
26
|
+
"ORDER BY name")
|
|
27
|
+
.all();
|
|
28
|
+
const enabled = providerRows.map((r) => r.name);
|
|
29
|
+
return {
|
|
30
|
+
protocol_version: PROTOCOL_VERSION,
|
|
31
|
+
plugin_api_version: PLUGIN_API_VERSION,
|
|
32
|
+
kernel_version: KERNEL_VERSION,
|
|
33
|
+
client_identifier_unverified: typeof input.client_identifier_unverified === "string" &&
|
|
34
|
+
input.client_identifier_unverified.length > 0
|
|
35
|
+
? input.client_identifier_unverified
|
|
36
|
+
: "unknown",
|
|
37
|
+
flag_vocabulary: Object.keys(FLAG_TO_PRESET),
|
|
38
|
+
transports: {
|
|
39
|
+
active: ACTIVE_TRANSPORT,
|
|
40
|
+
available: AVAILABLE_TRANSPORTS.slice(),
|
|
41
|
+
},
|
|
42
|
+
providers: {
|
|
43
|
+
enabled,
|
|
44
|
+
active_default: ACTIVE_DEFAULT_PROVIDER,
|
|
45
|
+
compatible_with_client: enabled.slice(),
|
|
46
|
+
},
|
|
47
|
+
bundles_available: bundleRows.map((r) => ({ name: r.name, version: r.version })),
|
|
48
|
+
sandbox: { kind: SANDBOX_KIND },
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","sourceRoot":"","sources":["../../../src/tools/meta.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,iEAAiE;AACjE,kEAAkE;AAClE,oDAAoD;AAEpD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAM3D,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AAEvB,MAAM,gBAAgB,GAAG,YAAY,CAAC;AACtC,MAAM,oBAAoB,GAAG,CAAC,gBAAgB,CAAC,CAAC;AAChD,MAAM,YAAY,GAAG,aAAa,CAAC;AACnC,qEAAqE;AACrE,mEAAmE;AACnE,yBAAyB;AACzB,MAAM,uBAAuB,GAAG,qBAAqB,CAAC;AAWtD,MAAM,UAAU,cAAc;IAC5B,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE;QACrB,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAErC,MAAM,YAAY,GAAG,EAAE;aACpB,OAAO,CACN,iDAAiD;YAC/C,iDAAiD;YACjD,eAAe,CAClB;aACA,GAAG,EAAyB,CAAC;QAEhC,MAAM,UAAU,GAAG,EAAE;aAClB,OAAO,CACN,iDAAiD;YAC/C,+CAA+C;YAC/C,eAAe,CAClB;aACA,GAAG,EAAyB,CAAC;QAEhC,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAEhD,OAAO;YACL,gBAAgB,EAAE,gBAAgB;YAClC,kBAAkB,EAAE,kBAAkB;YACtC,cAAc,EAAE,cAAc;YAC9B,4BAA4B,EAC1B,OAAO,KAAK,CAAC,4BAA4B,KAAK,QAAQ;gBACtD,KAAK,CAAC,4BAA4B,CAAC,MAAM,GAAG,CAAC;gBAC3C,CAAC,CAAC,KAAK,CAAC,4BAA4B;gBACpC,CAAC,CAAC,SAAS;YACf,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC;YAC5C,UAAU,EAAE;gBACV,MAAM,EAAE,gBAAgB;gBACxB,SAAS,EAAE,oBAAoB,CAAC,KAAK,EAAE;aACxC;YACD,SAAS,EAAE;gBACT,OAAO;gBACP,cAAc,EAAE,uBAAuB;gBACvC,sBAAsB,EAAE,OAAO,CAAC,KAAK,EAAE;aACxC;YACD,iBAAiB,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAChF,OAAO,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE;SAChC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Registry } from "@loomfsm/kernel";
|
|
2
|
+
import type { RecoverTaskInput, RecoverTaskResponse, ToolHandler } from "../types.js";
|
|
3
|
+
export interface RecoverDeps {
|
|
4
|
+
resolveRegistry?: (projectDir: string) => Promise<Registry> | Registry;
|
|
5
|
+
allowlistPath?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createRecoverTool(deps?: RecoverDeps): ToolHandler<RecoverTaskInput, RecoverTaskResponse>;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// pipeline_recover — the five-choice recovery surface.
|
|
2
|
+
//
|
|
3
|
+
// Composition mirrors pipeline_continue_task: project-dir allowlist gate
|
|
4
|
+
// → ledger replay lookup → recover inside one withStateTransaction (the
|
|
5
|
+
// owner check + the recovery-keyed ledger row + a co-committed audit row
|
|
6
|
+
// land with the state mutation) → for the re-entrant choices (retry /
|
|
7
|
+
// retry-failed / cancel-pending) resolve the next directive; for the
|
|
8
|
+
// terminal choices (abandon / force-close) shape the terminal response
|
|
9
|
+
// directly → materialize the cached response on the ledger row.
|
|
10
|
+
//
|
|
11
|
+
// Owner check + cross-owner marker: the owner comparison runs INSIDE the
|
|
12
|
+
// recovery tx via `ownerCheckGuard`. Same-owner / unclaimed tasks pass
|
|
13
|
+
// untouched. A cross-owner recovery is refused with CROSS_OWNER_REQUIRED
|
|
14
|
+
// unless the caller presents a signed `marker`; a presented marker is
|
|
15
|
+
// verified (signature, target, expiry, key) and CONSUMED in the same tx
|
|
16
|
+
// as the recovery — no read-then-act window. `client_identifier_unverified`
|
|
17
|
+
// stays forensics-only (the kernel never branches on it).
|
|
18
|
+
//
|
|
19
|
+
// retry-failed: the named pending rows are re-shuttled reusing their
|
|
20
|
+
// existing agent_run_id (no fresh begin_spawn → no duplicate-window
|
|
21
|
+
// trip), gated on provider idempotency — a non-idempotent provider is
|
|
22
|
+
// refused with PROVIDER_NOT_IDEMPOTENT.
|
|
23
|
+
//
|
|
24
|
+
// recovery_id is server-issued: omit it on the first call (the kernel
|
|
25
|
+
// mints one and returns it); pass it back to replay the cached response
|
|
26
|
+
// verbatim; omit it to issue a NEW recovery action. The response always
|
|
27
|
+
// carries the (minted-or-supplied) recovery_id so a client retry is
|
|
28
|
+
// keyable — even when the response is an error envelope.
|
|
29
|
+
//
|
|
30
|
+
// Refusals (allowlist, owner / marker, invalid/stale/terminal recovery,
|
|
31
|
+
// missing registry) are error-shaped wire envelopes; only programmer
|
|
32
|
+
// errors throw.
|
|
33
|
+
import { assertProjectDirAllowed, buildRetryFailedDirective, captureNow, KernelError, loadState, makeRecoveryId, openDb, ownerCheckGuard, readLedgerRow, recoverTask, runFSM, TransactionImpl, withStateTransaction, writeLedgerRow, } from "@loomfsm/kernel";
|
|
34
|
+
import { createTransportAdapter } from "../transport-adapter.js";
|
|
35
|
+
const REENTRANT = new Set([
|
|
36
|
+
"retry",
|
|
37
|
+
"retry-failed",
|
|
38
|
+
"cancel-pending",
|
|
39
|
+
]);
|
|
40
|
+
const DEFAULT_OWNER = "anonymous";
|
|
41
|
+
export function createRecoverTool(deps = {}) {
|
|
42
|
+
const adapter = createTransportAdapter();
|
|
43
|
+
return async (input) => {
|
|
44
|
+
const driverStateId = input.driver_state_id;
|
|
45
|
+
// recovery_id is resolved BEFORE any work so every exit path — refusal
|
|
46
|
+
// or success — echoes a keyable id back to the caller.
|
|
47
|
+
const recoveryId = resolveRecoveryId(input.recovery_id);
|
|
48
|
+
// 1. Project-dir allowlist.
|
|
49
|
+
try {
|
|
50
|
+
await assertProjectDirAllowed(input.project_dir, deps.allowlistPath !== undefined ? { allowlistPath: deps.allowlistPath } : undefined);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return refusal(err, driverStateId, recoveryId);
|
|
54
|
+
}
|
|
55
|
+
const callerOwner = typeof input.owner_id === "string" && input.owner_id.length > 0
|
|
56
|
+
? input.owner_id
|
|
57
|
+
: DEFAULT_OWNER;
|
|
58
|
+
const marker = toBypassMarker(input.marker);
|
|
59
|
+
// 2. Replay — a materialized ledger blob for this exact recovery action
|
|
60
|
+
// replays the cached envelope verbatim (same recovery_id echoed).
|
|
61
|
+
const ledgerKey = `recovery:${driverStateId}:${input.choice}:${recoveryId}`;
|
|
62
|
+
const cached = await readCachedRecovery(input.project_dir, ledgerKey);
|
|
63
|
+
if (cached !== null) {
|
|
64
|
+
return { response: cached, recovery_id: recoveryId };
|
|
65
|
+
}
|
|
66
|
+
// 3. The re-entrant choices need a flow to tick; without a registry
|
|
67
|
+
// they refuse. Terminal choices (abandon / force-close) shape a
|
|
68
|
+
// terminal response with no FSM tick, so they proceed regardless.
|
|
69
|
+
const reentrant = REENTRANT.has(input.choice);
|
|
70
|
+
if (reentrant && deps.resolveRegistry === undefined) {
|
|
71
|
+
return {
|
|
72
|
+
response: errorResponse(driverStateId, "REGISTRY_UNAVAILABLE", "no registry resolver is wired for the re-entrant recovery path"),
|
|
73
|
+
recovery_id: recoveryId,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const identifier = typeof input.client_identifier_unverified === "string" &&
|
|
77
|
+
input.client_identifier_unverified.length > 0
|
|
78
|
+
? input.client_identifier_unverified
|
|
79
|
+
: "unknown";
|
|
80
|
+
// 4. Owner check + recover + co-committed audit row, all in one tx. The
|
|
81
|
+
// cross-owner marker (if any) is verified and CONSUMED here, atomic
|
|
82
|
+
// with the recovery it authorizes.
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = await withStateTransaction(input.project_dir, captureNow(), async (tx) => {
|
|
86
|
+
await ownerCheckGuard(tx, { driver_state_id: driverStateId, caller_owner_id: callerOwner }, marker);
|
|
87
|
+
const recovered = await recoverTask(tx, {
|
|
88
|
+
driver_state_id: driverStateId,
|
|
89
|
+
choice: input.choice,
|
|
90
|
+
...(input.agent_run_ids !== undefined ? { agent_run_ids: input.agent_run_ids } : {}),
|
|
91
|
+
recovery_id: recoveryId,
|
|
92
|
+
});
|
|
93
|
+
const taskId = await readTaskId(tx);
|
|
94
|
+
await writeAuditRow(tx, "pipeline_recover", taskId, driverStateId, {
|
|
95
|
+
client_identifier_unverified: identifier,
|
|
96
|
+
choice: input.choice,
|
|
97
|
+
recovery_id: recoveryId,
|
|
98
|
+
}, outcomeErrorClass(recovered.outcome));
|
|
99
|
+
return recovered;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return refusal(err, driverStateId, recoveryId);
|
|
104
|
+
}
|
|
105
|
+
// 5. Shape the response: re-entrant choices resume the FSM (retry /
|
|
106
|
+
// cancel-pending) or re-shuttle the named pending rows (retry-failed);
|
|
107
|
+
// terminal choices read the now-closed state and shape directly. The
|
|
108
|
+
// recovery has already committed, so a kernel-coded failure of the
|
|
109
|
+
// re-entry (e.g. PROVIDER_NOT_IDEMPOTENT on a retry-failed against a
|
|
110
|
+
// non-idempotent provider) is shaped as an error envelope, not
|
|
111
|
+
// thrown — the cached envelope makes a replay return the same outcome.
|
|
112
|
+
let response;
|
|
113
|
+
if (result.reenter) {
|
|
114
|
+
try {
|
|
115
|
+
const registry = await deps.resolveRegistry(input.project_dir);
|
|
116
|
+
const loaded = await readState(input.project_dir);
|
|
117
|
+
if (input.choice === "retry-failed") {
|
|
118
|
+
const directive = buildRetryFailedDirective(loaded, registry, input.agent_run_ids ?? []);
|
|
119
|
+
response = adapter.shape(directive, { driver_state_id: driverStateId });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const { directive } = await runFSM(loaded, registry);
|
|
123
|
+
response = adapter.shape(directive, { driver_state_id: driverStateId });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
if (!(err instanceof KernelError))
|
|
128
|
+
throw err;
|
|
129
|
+
response = errorResponse(driverStateId, err.code, err.message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
response = await terminalResponse(input.project_dir, input.choice);
|
|
134
|
+
}
|
|
135
|
+
// 6. Materialize the cached response on the recovery ledger row.
|
|
136
|
+
await withStateTransaction(input.project_dir, captureNow(), async (tx) => {
|
|
137
|
+
const taskId = await readTaskId(tx);
|
|
138
|
+
await writeLedgerRow(tx, ledgerKey, {
|
|
139
|
+
driver_state_id: driverStateId,
|
|
140
|
+
task_id: taskId,
|
|
141
|
+
response_blob: JSON.stringify(response),
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
return { response, recovery_id: recoveryId };
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function resolveRecoveryId(supplied) {
|
|
148
|
+
if (typeof supplied === "string" && supplied.length > 0)
|
|
149
|
+
return supplied;
|
|
150
|
+
return makeRecoveryId();
|
|
151
|
+
}
|
|
152
|
+
// Map the wire marker (plain strings) onto the kernel BypassMarker
|
|
153
|
+
// (NowToken-branded timestamps). The kernel re-derives the HMAC and
|
|
154
|
+
// verifies it — a tampered field simply fails the signature check.
|
|
155
|
+
function toBypassMarker(input) {
|
|
156
|
+
if (input === undefined)
|
|
157
|
+
return undefined;
|
|
158
|
+
return {
|
|
159
|
+
issued_at: input.issued_at,
|
|
160
|
+
expires_at: input.expires_at,
|
|
161
|
+
reason: input.reason,
|
|
162
|
+
hmac: input.hmac,
|
|
163
|
+
key_id: input.key_id,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// The forensic recovery outcome maps to the audit error_class: an
|
|
167
|
+
// idempotent / raced recovery is a successful no-op tagged for forensics,
|
|
168
|
+
// not a failure — verdict stays 'ok' and the tag rides on error_class.
|
|
169
|
+
function outcomeErrorClass(outcome) {
|
|
170
|
+
switch (outcome) {
|
|
171
|
+
case "idempotent":
|
|
172
|
+
return "recovery-idempotent";
|
|
173
|
+
case "raced":
|
|
174
|
+
return "recovery-raced";
|
|
175
|
+
case "applied":
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function readCachedRecovery(projectDir, key) {
|
|
180
|
+
const db = openDb(projectDir);
|
|
181
|
+
const tx = new TransactionImpl(db, captureNow());
|
|
182
|
+
const row = await readLedgerRow(tx, key);
|
|
183
|
+
if (row === null || row.response_blob === null)
|
|
184
|
+
return null;
|
|
185
|
+
return JSON.parse(row.response_blob);
|
|
186
|
+
}
|
|
187
|
+
async function terminalResponse(projectDir, choice) {
|
|
188
|
+
const db = openDb(projectDir);
|
|
189
|
+
const tx = new TransactionImpl(db, captureNow());
|
|
190
|
+
const taskId = await readTaskId(tx);
|
|
191
|
+
if (choice === "force-close") {
|
|
192
|
+
return {
|
|
193
|
+
status: "complete",
|
|
194
|
+
task_id: taskId,
|
|
195
|
+
verdict: "failed_force_closed",
|
|
196
|
+
summary: "task force-closed via recovery",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// abandon — the canonical pipeline verdict is NULL (status='abandoned').
|
|
200
|
+
// The wire `complete` form has no abandoned/null verdict, so the wire
|
|
201
|
+
// verdict maps to 'rejected' (the abandon-intent terminal); the stored
|
|
202
|
+
// verdict stays NULL and is distinct from a force-close.
|
|
203
|
+
return {
|
|
204
|
+
status: "complete",
|
|
205
|
+
task_id: taskId,
|
|
206
|
+
verdict: "rejected",
|
|
207
|
+
summary: "task abandoned via recovery",
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async function readState(projectDir) {
|
|
211
|
+
const db = openDb(projectDir);
|
|
212
|
+
const tx = new TransactionImpl(db, captureNow());
|
|
213
|
+
return await loadState(tx);
|
|
214
|
+
}
|
|
215
|
+
async function readTaskId(tx) {
|
|
216
|
+
const row = await tx.queryRow("SELECT task_id FROM pipeline_state WHERE id = 1");
|
|
217
|
+
if (row === null || row.task_id === null)
|
|
218
|
+
return null;
|
|
219
|
+
return String(row.task_id);
|
|
220
|
+
}
|
|
221
|
+
async function writeAuditRow(tx, type, taskId, driverStateId, payload, errorClass) {
|
|
222
|
+
await tx.exec("INSERT INTO audit (ts, type, task_id, driver_state_id, payload, verdict, error_class) " +
|
|
223
|
+
"VALUES (?, ?, ?, ?, ?, 'ok', ?)", [tx.now, type, taskId, driverStateId, JSON.stringify(payload), errorClass]);
|
|
224
|
+
}
|
|
225
|
+
function refusal(err, driverStateId, recoveryId) {
|
|
226
|
+
if (err instanceof KernelError) {
|
|
227
|
+
return {
|
|
228
|
+
response: errorResponse(driverStateId, err.code, err.message),
|
|
229
|
+
recovery_id: recoveryId,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
function errorResponse(driverStateId, code, message) {
|
|
235
|
+
return {
|
|
236
|
+
status: "error",
|
|
237
|
+
driver_state_id: driverStateId,
|
|
238
|
+
code,
|
|
239
|
+
message,
|
|
240
|
+
recovery_options: [],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=recover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recover.js","sourceRoot":"","sources":["../../../src/tools/recover.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// pipeline_restore — restore project state from a backup.
|
|
2
|
+
//
|
|
3
|
+
// Two formats:
|
|
4
|
+
// sql — the dump is UNTRUSTED input. It is parsed through the DDL
|
|
5
|
+
// allowlist (parseRestoreSql), which classifies each statement
|
|
6
|
+
// and refuses anything outside the allowed set; the parsed,
|
|
7
|
+
// allowlisted statements are then applied inside one
|
|
8
|
+
// withStateTransaction. A backup file never reaches
|
|
9
|
+
// `db.exec(rawSql)` — an out-of-allowlist statement surfaces
|
|
10
|
+
// RESTORE_REJECTED naming the offender.
|
|
11
|
+
// binary — operator-explicit ("trust the source file"): close the
|
|
12
|
+
// project connection and copy the .db over the project's
|
|
13
|
+
// state.db with no kernel validation. The next kernel start
|
|
14
|
+
// re-applies migrations and refuses on schema mismatch.
|
|
15
|
+
//
|
|
16
|
+
// Both formats refuse without confirm:true (RESTORE_CONFIRM_REQUIRED) and
|
|
17
|
+
// thread the NowToken for `ts`.
|
|
18
|
+
import { copyFileSync, readFileSync } from "node:fs";
|
|
19
|
+
import { join, resolve } from "node:path";
|
|
20
|
+
import { applyRestoreStatements, assertProjectDirAllowed, captureNow, closeDb, KernelError, parseRestoreSql, withStateTransaction, } from "@loomfsm/kernel";
|
|
21
|
+
export function createRestoreTool(deps = {}) {
|
|
22
|
+
return async (input) => {
|
|
23
|
+
const now = captureNow();
|
|
24
|
+
// 1. Project-dir allowlist.
|
|
25
|
+
try {
|
|
26
|
+
await assertProjectDirAllowed(input.project_dir, deps.allowlistPath !== undefined ? { allowlistPath: deps.allowlistPath } : undefined);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return refusal(err, now);
|
|
30
|
+
}
|
|
31
|
+
// 2. A restore overwrites canonical state — require explicit confirm.
|
|
32
|
+
if (input.confirm !== true) {
|
|
33
|
+
return {
|
|
34
|
+
restored: false,
|
|
35
|
+
ts: now,
|
|
36
|
+
error: {
|
|
37
|
+
code: "RESTORE_CONFIRM_REQUIRED",
|
|
38
|
+
message: "pipeline_restore overwrites state and requires confirm:true",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const from = resolve(input.project_dir, input.from);
|
|
43
|
+
if (input.format === "binary") {
|
|
44
|
+
// Operator-explicit file swap — no kernel validation.
|
|
45
|
+
try {
|
|
46
|
+
closeDb(input.project_dir);
|
|
47
|
+
const dest = join(input.project_dir, ".claude", "state.db");
|
|
48
|
+
copyFileSync(from, dest);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return refusal(err, now);
|
|
52
|
+
}
|
|
53
|
+
return { restored: true, ts: now };
|
|
54
|
+
}
|
|
55
|
+
// SQL path — parse the untrusted dump, then apply the allowlisted set.
|
|
56
|
+
let statements;
|
|
57
|
+
try {
|
|
58
|
+
const sql = readFileSync(from, "utf8");
|
|
59
|
+
statements = parseRestoreSql(sql);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return refusal(err, now);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await withStateTransaction(input.project_dir, now, (tx) => applyRestoreStatements(tx, statements));
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return refusal(err, now);
|
|
69
|
+
}
|
|
70
|
+
return { restored: true, ts: now };
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function refusal(err, ts) {
|
|
74
|
+
if (err instanceof KernelError) {
|
|
75
|
+
return { restored: false, ts, error: { code: err.code, message: err.message } };
|
|
76
|
+
}
|
|
77
|
+
if (err instanceof Error) {
|
|
78
|
+
return { restored: false, ts, error: { code: "RESTORE_FAILED", message: err.message } };
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=restore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore.js","sourceRoot":"","sources":["../../../src/tools/restore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Registry } from "@loomfsm/kernel";
|
|
2
|
+
import type { RunTaskInput, RunTaskResponse, ToolHandler } from "../types.js";
|
|
3
|
+
export interface RunTaskDeps {
|
|
4
|
+
resolveRegistry?: (projectDir: string) => Promise<Registry> | Registry;
|
|
5
|
+
allowlistPath?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createRunTaskTool(deps?: RunTaskDeps): ToolHandler<RunTaskInput, RunTaskResponse>;
|