warpmetal 0.8.13 → 0.9.1
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 +98 -4
- package/docs/agent-insights-cli.md +123 -0
- package/docs/agent-work-cli.md +72 -0
- package/docs/session-handoff-transport.md +68 -0
- package/package.json +5 -3
- package/skills/warpmetal/SKILL.md +54 -1
- package/skills/warpmetal/references/cli-reference.md +150 -2
- package/skills/warpmetal/references/runtime.md +19 -0
- package/src/account-gateway.js +217 -0
- package/src/account-session.js +209 -0
- package/src/agent-insights.js +638 -0
- package/src/agent-management.js +146 -0
- package/src/agent-work.js +800 -0
- package/src/api.js +10 -4
- package/src/cli.js +398 -11
- package/src/contracts/agent-manager-control-v1.schema.json +563 -0
- package/src/contracts/http.js +3 -0
- package/src/contracts/insights.js +133 -0
- package/src/contracts/manager.js +86 -0
- package/src/contracts/session-handoff.js +49 -0
- package/src/contracts/sources.json +12 -0
- package/src/contracts/work-handoff.js +73 -0
- package/src/contracts/work.js +310 -0
- package/src/customer-auth.js +329 -0
- package/src/installer.js +5 -5
- package/src/models.js +85 -0
- package/src/runtime.js +76 -3
- package/src/session-handoff.js +915 -0
- package/src/state.js +20 -4
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { chmod, lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { WarpMetalClient } from "./api.js";
|
|
6
|
+
import { ApiError, CliError } from "./errors.js";
|
|
7
|
+
|
|
8
|
+
const MAX_RESPONSE = 2 * 1024 * 1024;
|
|
9
|
+
const safeCode = value => typeof value === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(value) ? value : "agent_management_unavailable";
|
|
10
|
+
const hash = value => createHash("sha256").update(value).digest("hex");
|
|
11
|
+
const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
|
|
12
|
+
: value !== null && typeof value === "object"
|
|
13
|
+
? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`
|
|
14
|
+
: JSON.stringify(value);
|
|
15
|
+
|
|
16
|
+
export async function readManagementInput(path, maxBytes = 64 * 1024) {
|
|
17
|
+
let file;
|
|
18
|
+
try {
|
|
19
|
+
if (typeof path !== "string" || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_RESPONSE) throw new Error();
|
|
20
|
+
file = await open(resolve(path), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
21
|
+
const info = await file.stat();
|
|
22
|
+
if (!info.isFile() || info.size > maxBytes) throw new Error();
|
|
23
|
+
const bytes = Buffer.alloc(maxBytes + 1);
|
|
24
|
+
let length = 0;
|
|
25
|
+
while (length < bytes.length) {
|
|
26
|
+
const result = await file.read(bytes, length, bytes.length - length, length);
|
|
27
|
+
if (result.bytesRead === 0) break;
|
|
28
|
+
length += result.bytesRead;
|
|
29
|
+
}
|
|
30
|
+
if (length > maxBytes) throw new Error();
|
|
31
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length)));
|
|
32
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
|
|
33
|
+
return value;
|
|
34
|
+
} catch {
|
|
35
|
+
throw new CliError("Input must be a bounded JSON object in a regular file.", { exitCode: 2, code: "invalid_management_input" });
|
|
36
|
+
} finally {
|
|
37
|
+
await file?.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function managementClient({ baseUrl, fetchImpl }) {
|
|
42
|
+
const client = new WarpMetalClient({ baseUrl, fetchImpl: async (url, options) => {
|
|
43
|
+
const response = await fetchImpl(url, { ...options, redirect: "manual" });
|
|
44
|
+
if (response.status >= 300 && response.status < 400) throw new Error("redirect refused");
|
|
45
|
+
const reader = response.body?.getReader();
|
|
46
|
+
let length = 0;
|
|
47
|
+
const chunks = [];
|
|
48
|
+
try {
|
|
49
|
+
if (reader) for (;;) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done) break;
|
|
52
|
+
length += value.byteLength;
|
|
53
|
+
if (length > MAX_RESPONSE) {
|
|
54
|
+
await reader.cancel();
|
|
55
|
+
throw new Error("response exceeds bound");
|
|
56
|
+
}
|
|
57
|
+
chunks.push(Buffer.from(value));
|
|
58
|
+
}
|
|
59
|
+
} finally { reader?.releaseLock(); }
|
|
60
|
+
const body = [204, 205, 304].includes(response.status) ? null : Buffer.concat(chunks);
|
|
61
|
+
return new Response(body, { status: response.status, headers: response.headers });
|
|
62
|
+
} });
|
|
63
|
+
return {
|
|
64
|
+
baseUrl: client.baseUrl,
|
|
65
|
+
async request(...args) {
|
|
66
|
+
try { return await client.request(...args); }
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof ApiError) throw new ApiError("Agent management request was refused.", {
|
|
69
|
+
status: error.status, code: safeCode(error.code),
|
|
70
|
+
retryAfter: /^\d{1,6}$/.test(String(error.retryAfter)) ? error.retryAfter : undefined,
|
|
71
|
+
});
|
|
72
|
+
throw new CliError("Agent management response is unavailable; reconcile the same request ID.", {
|
|
73
|
+
exitCode: 3, code: "agent_management_unavailable",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function syncDirectory(directory) {
|
|
81
|
+
if (process.platform === "win32") return;
|
|
82
|
+
const handle = await open(directory, "r");
|
|
83
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function saveRecord(path, record, exclusive = false) {
|
|
87
|
+
const target = exclusive ? path : `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
88
|
+
let handle;
|
|
89
|
+
try {
|
|
90
|
+
handle = await open(target, "wx", 0o600);
|
|
91
|
+
await handle.writeFile(`${JSON.stringify(record)}\n`);
|
|
92
|
+
await handle.sync();
|
|
93
|
+
await handle.close();
|
|
94
|
+
handle = undefined;
|
|
95
|
+
if (!exclusive) await rename(target, path);
|
|
96
|
+
} finally {
|
|
97
|
+
await handle?.close();
|
|
98
|
+
if (!exclusive) await rm(target, { force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Private opaque intent only. A saved intent never causes another mutation. */
|
|
103
|
+
export function managementMutations(stateDirectory, origin) {
|
|
104
|
+
const directory = join(stateDirectory, "agent-management-operations");
|
|
105
|
+
return async ({ path, serverId, sandboxId, kind, requestId, body, submit, reconcile }) => {
|
|
106
|
+
if (typeof path !== "string" || !path.startsWith("/servers/") || path.includes("?") ||
|
|
107
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{3,159}$/.test(requestId || "") ||
|
|
108
|
+
typeof submit !== "function" || typeof reconcile !== "function") {
|
|
109
|
+
throw new CliError("A fixed route and recoverable request ID are required.", { exitCode: 2, code: "invalid_management_intent" });
|
|
110
|
+
}
|
|
111
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
112
|
+
if (!(await lstat(directory)).isDirectory()) throw new CliError("Management intent storage is unavailable.", { exitCode: 3 });
|
|
113
|
+
if (process.platform !== "win32") await chmod(directory, 0o700);
|
|
114
|
+
const file = join(directory, `${hash(canonical({ origin, path, requestId }))}.json`);
|
|
115
|
+
const identity = { version: 1, origin, path, serverId, sandboxId, kind, requestId, bodyDigest: hash(canonical(body)) };
|
|
116
|
+
let record = { ...identity, state: "dispatching" };
|
|
117
|
+
let fresh = false;
|
|
118
|
+
try {
|
|
119
|
+
await saveRecord(file, record, true);
|
|
120
|
+
await syncDirectory(directory);
|
|
121
|
+
fresh = true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error.code !== "EEXIST") throw new CliError("Could not persist management intent; no request was sent.", { exitCode: 3, code: "management_storage_unavailable" });
|
|
124
|
+
try {
|
|
125
|
+
const info = await lstat(file);
|
|
126
|
+
if (!info.isFile() || info.size > 8192 || process.platform !== "win32" && (info.mode & 0o077)) throw new Error();
|
|
127
|
+
record = JSON.parse(await readFile(file, "utf8"));
|
|
128
|
+
} catch {
|
|
129
|
+
throw new CliError("Saved management intent is unavailable; no mutation was sent.", { exitCode: 3, code: "management_storage_unavailable" });
|
|
130
|
+
}
|
|
131
|
+
if (Object.entries(identity).some(([key, value]) => record[key] !== value)) {
|
|
132
|
+
throw new CliError("This request ID already names a different management intent.", { exitCode: 5, code: "management_intent_conflict" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
let result;
|
|
136
|
+
try { result = await (fresh ? submit() : reconcile()); }
|
|
137
|
+
catch (error) {
|
|
138
|
+
await saveRecord(file, { ...identity, state: "outcome_unknown" }).catch(() => {});
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
// Keep no response, prompt, content, authentication or local file path.
|
|
142
|
+
await saveRecord(file, { ...identity, state: "submitted" });
|
|
143
|
+
await syncDirectory(directory);
|
|
144
|
+
return result;
|
|
145
|
+
};
|
|
146
|
+
}
|