harnery 0.8.0 → 0.10.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/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +8 -0
- package/dist/commands/agents.d.ts +24 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +244 -13
- package/dist/commands/backup.d.ts +5 -4
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +15 -14
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +27 -1
- package/dist/commands/claude-desktop.d.ts +19 -0
- package/dist/commands/claude-desktop.d.ts.map +1 -0
- package/dist/commands/claude-desktop.js +168 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +149 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +37 -0
- package/dist/commands/eml.d.ts +32 -0
- package/dist/commands/eml.d.ts.map +1 -1
- package/dist/commands/eml.js +16 -3
- package/dist/commands/grep.d.ts +35 -2
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +427 -139
- package/dist/commands/harness.d.ts +11 -0
- package/dist/commands/harness.d.ts.map +1 -0
- package/dist/commands/harness.js +118 -0
- package/dist/commands/init.d.ts +15 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +125 -14
- package/dist/commands/presence.d.ts +9 -4
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +88 -5
- package/dist/commands/relay.d.ts +9 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +143 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -0
- package/dist/commands/workflow.d.ts +4 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +82 -0
- package/dist/core/agents/canonical-emit.d.ts +15 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +41 -3
- package/dist/core/agents/cli.js +14 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +44 -3
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +26 -1
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +26 -1
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +4 -37
- package/dist/core/agents/rules/stop-hook.d.ts +8 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +8 -0
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +16 -43
- package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +24 -4
- package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +24 -7
- package/dist/core/agents/state/names.d.ts +38 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +46 -7
- package/dist/core/agents/state/pidmap.d.ts +5 -1
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +58 -2
- package/dist/core/agents/state/stale-sweep.d.ts +3 -2
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +6 -6
- package/dist/core/config.d.ts +77 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +210 -25
- package/dist/core/context/index.d.ts +144 -0
- package/dist/core/context/index.d.ts.map +1 -0
- package/dist/core/context/index.js +380 -0
- package/dist/core/harnesses/bench.d.ts +29 -0
- package/dist/core/harnesses/bench.d.ts.map +1 -0
- package/dist/core/harnesses/bench.js +151 -0
- package/dist/core/harnesses/index.d.ts +9 -0
- package/dist/core/harnesses/index.d.ts.map +1 -0
- package/dist/core/harnesses/index.js +4 -0
- package/dist/core/harnesses/profiles.d.ts +62 -0
- package/dist/core/harnesses/profiles.d.ts.map +1 -0
- package/dist/core/harnesses/profiles.js +115 -0
- package/dist/core/harnesses/registry.d.ts +15 -0
- package/dist/core/harnesses/registry.d.ts.map +1 -0
- package/dist/core/harnesses/registry.js +113 -0
- package/dist/core/harnesses/types.d.ts +64 -0
- package/dist/core/harnesses/types.d.ts.map +1 -0
- package/dist/core/harnesses/types.js +20 -0
- package/dist/core/hooks/cli.js +309 -66
- package/dist/core/hooks/events/schema.d.ts +42 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts +7 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +4 -0
- package/dist/core/hooks/harness/parse.d.ts +1 -1
- package/dist/core/hooks/harness/parse.d.ts.map +1 -1
- package/dist/core/hooks/harness/parse.js +4 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
- package/dist/core/hooks/resolve/coord-root.js +28 -6
- package/dist/core/presence/blob.d.ts +40 -0
- package/dist/core/presence/blob.d.ts.map +1 -0
- package/dist/core/presence/blob.js +91 -0
- package/dist/core/presence/git.d.ts +68 -0
- package/dist/core/presence/git.d.ts.map +1 -0
- package/dist/core/presence/git.js +173 -0
- package/dist/core/presence/index.d.ts +74 -0
- package/dist/core/presence/index.d.ts.map +1 -0
- package/dist/core/presence/index.js +224 -0
- package/dist/core/presence/relay-client.d.ts +39 -0
- package/dist/core/presence/relay-client.d.ts.map +1 -0
- package/dist/core/presence/relay-client.js +295 -0
- package/dist/core/presence/relay-protocol.d.ts +94 -0
- package/dist/core/presence/relay-protocol.d.ts.map +1 -0
- package/dist/core/presence/relay-protocol.js +169 -0
- package/dist/core/workflow/billing.d.ts +48 -0
- package/dist/core/workflow/billing.d.ts.map +1 -0
- package/dist/core/workflow/billing.js +108 -0
- package/dist/core/workflow/child-env.d.ts +30 -0
- package/dist/core/workflow/child-env.d.ts.map +1 -0
- package/dist/core/workflow/child-env.js +43 -0
- package/dist/core/workflow/engine.d.ts +21 -0
- package/dist/core/workflow/engine.d.ts.map +1 -0
- package/dist/core/workflow/engine.js +349 -0
- package/dist/core/workflow/harnesses.d.ts +17 -0
- package/dist/core/workflow/harnesses.d.ts.map +1 -0
- package/dist/core/workflow/harnesses.js +21 -0
- package/dist/core/workflow/spawn-claude.d.ts +25 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
- package/dist/core/workflow/spawn-claude.js +103 -0
- package/dist/core/workflow/spawn-codex.d.ts +22 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
- package/dist/core/workflow/spawn-codex.js +88 -0
- package/dist/core/workflow/spawn-cursor.d.ts +29 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
- package/dist/core/workflow/spawn-cursor.js +91 -0
- package/dist/core/workflow/types.d.ts +152 -0
- package/dist/core/workflow/types.d.ts.map +1 -0
- package/dist/core/workflow/types.js +9 -0
- package/dist/core/workflow/validate.d.ts +15 -0
- package/dist/core/workflow/validate.d.ts.map +1 -0
- package/dist/core/workflow/validate.js +70 -0
- package/dist/lib/browser/client.d.ts +16 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +22 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -0
- package/dist/lib/browser/launch-args.d.ts +21 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -0
- package/dist/lib/browser/launch-args.js +32 -0
- package/dist/lib/claude-desktop.d.ts +105 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -0
- package/dist/lib/claude-desktop.js +217 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +5 -2
- package/dist/lib/identities/assume.d.ts +51 -0
- package/dist/lib/identities/assume.d.ts.map +1 -0
- package/dist/lib/identities/assume.js +274 -0
- package/dist/lib/identities/index.d.ts +7 -7
- package/dist/lib/identities/index.d.ts.map +1 -1
- package/dist/lib/identities/index.js +24 -24
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +15 -2
- package/package.json +12 -1
- package/schemas/config.schema.json +88 -19
- package/src/commander.ts +8 -0
- package/src/commands/agents.ts +280 -14
- package/src/commands/backup.ts +17 -16
- package/src/commands/browse.ts +32 -0
- package/src/commands/claude-desktop.ts +215 -0
- package/src/commands/context.ts +174 -1
- package/src/commands/doctor.ts +44 -0
- package/src/commands/eml.ts +22 -4
- package/src/commands/grep.ts +535 -142
- package/src/commands/harness.ts +147 -0
- package/src/commands/init.ts +138 -17
- package/src/commands/presence.ts +111 -5
- package/src/commands/relay.ts +160 -0
- package/src/commands/sync.ts +4 -0
- package/src/commands/workflow.ts +123 -0
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +15 -6
- package/src/core/agents/render/prompt-context.ts +48 -3
- package/src/core/agents/render/session-context.ts +27 -1
- package/src/core/agents/rules/claim-conflict.ts +26 -1
- package/src/core/agents/rules/commit-conflict.ts +4 -33
- package/src/core/agents/rules/stop-hook.ts +17 -0
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +23 -2
- package/src/core/agents/state/heartbeat-writer.ts +33 -7
- package/src/core/agents/state/names.ts +68 -9
- package/src/core/agents/state/pidmap.ts +59 -2
- package/src/core/agents/state/stale-sweep.ts +6 -11
- package/src/core/config.ts +271 -24
- package/src/core/context/index.ts +575 -0
- package/src/core/harnesses/bench.ts +214 -0
- package/src/core/harnesses/index.ts +31 -0
- package/src/core/harnesses/profiles.ts +141 -0
- package/src/core/harnesses/registry.ts +140 -0
- package/src/core/harnesses/types.ts +90 -0
- package/src/core/hooks/cli.ts +361 -65
- package/src/core/hooks/events/schema.ts +72 -0
- package/src/core/hooks/harness/events.ts +11 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- package/src/core/hooks/resolve/coord-root.ts +28 -6
- package/src/core/presence/blob.ts +125 -0
- package/src/core/presence/git.ts +191 -0
- package/src/core/presence/index.ts +274 -0
- package/src/core/presence/relay-client.ts +322 -0
- package/src/core/presence/relay-protocol.ts +246 -0
- package/src/core/workflow/billing.ts +152 -0
- package/src/core/workflow/child-env.ts +47 -0
- package/src/core/workflow/engine.ts +403 -0
- package/src/core/workflow/harnesses.ts +33 -0
- package/src/core/workflow/spawn-claude.ts +118 -0
- package/src/core/workflow/spawn-codex.ts +93 -0
- package/src/core/workflow/spawn-cursor.ts +108 -0
- package/src/core/workflow/types.ts +160 -0
- package/src/core/workflow/validate.ts +75 -0
- package/src/lib/browser/client.ts +37 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/launch-args.ts +33 -0
- package/src/lib/claude-desktop.ts +293 -0
- package/src/lib/docs-lint.ts +5 -2
- package/src/lib/identities/assume.ts +363 -0
- package/src/lib/identities/index.ts +28 -24
- package/src/lib/instructions/templates.ts +16 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay protocol for cross-machine presence (ADR 0016, phase 2): the shared,
|
|
3
|
+
* host-agnostic logic both relay hosts (the Cloudflare Durable Objects worker
|
|
4
|
+
* and `harn relay serve`) and the client transport build on.
|
|
5
|
+
*
|
|
6
|
+
* Capability-based rooms: the room id and the E2E symmetric key are BOTH
|
|
7
|
+
* derived client-side via HKDF-SHA256 from (repo root-commit SHA, normalized
|
|
8
|
+
* origin URL, a rotatable salt). Anyone who can read the repo can derive them;
|
|
9
|
+
* nobody else can — including the relay operator, who sees only an opaque
|
|
10
|
+
* room id, opaque sender ids, and AES-GCM ciphertext. Rotating the salt
|
|
11
|
+
* rotates the room.
|
|
12
|
+
*
|
|
13
|
+
* The committed salt is NOT a secret and must never be treated as one: the
|
|
14
|
+
* derivation needs the root-commit SHA + origin URL too, which only
|
|
15
|
+
* repo-readers have. Repo access is the trust boundary (same as the git-refs
|
|
16
|
+
* transport).
|
|
17
|
+
*
|
|
18
|
+
* Wire format (JSON text frames over WebSocket):
|
|
19
|
+
* client → relay: { t: "pub", sender, iv, ct } broadcast + cache
|
|
20
|
+
* relay → client: { t: "pub", sender, iv, ct } live or warm-join replay
|
|
21
|
+
* relay → client: { t: "hello", peers } on join (peer count)
|
|
22
|
+
* The relay never sees plaintext: `ct` is AES-GCM over the presence-blob JSON,
|
|
23
|
+
* `sender` is an HMAC-derived opaque id, `iv` is the per-message nonce.
|
|
24
|
+
*
|
|
25
|
+
* Uses WebCrypto only (globalThis.crypto.subtle) so the same module runs on
|
|
26
|
+
* Bun, Node ≥ 20, and Cloudflare Workers.
|
|
27
|
+
*/
|
|
28
|
+
const ROOM_INFO = "harnery-presence/room-id/v1";
|
|
29
|
+
const KEY_INFO = "harnery-presence/room-key/v1";
|
|
30
|
+
const SENDER_INFO = "harnery-presence/sender-id/v1";
|
|
31
|
+
const HKDF_SALT = "harnery-presence/hkdf-salt/v1";
|
|
32
|
+
/** Default derivation salt when a repo hasn't minted `.harnery/presence-salt`.
|
|
33
|
+
* Public by design (see module docs); committing a random per-repo salt just
|
|
34
|
+
* rotates the room away from anything derived before. */
|
|
35
|
+
export const DEFAULT_ROOM_SALT = "harnery-presence-salt/v1";
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a git remote URL so every clone shape of the same repo derives
|
|
38
|
+
* the same room: `git@github.com:Org/Repo.git`, `ssh://git@github.com/org/repo`,
|
|
39
|
+
* and `https://github.com/org/repo.git` all → `github.com/org/repo`.
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeOriginUrl(raw) {
|
|
42
|
+
let s = raw.trim();
|
|
43
|
+
// scp-like syntax: [user@]host:path
|
|
44
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):(?!\/\/)(.+)$/.exec(s);
|
|
45
|
+
if (scp) {
|
|
46
|
+
s = `${scp[1]}/${scp[2]}`;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// URL syntax: strip scheme, then credentials.
|
|
50
|
+
s = s.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
|
|
51
|
+
s = s.replace(/^[^@/]+@/, "");
|
|
52
|
+
}
|
|
53
|
+
// Drop an explicit port, trailing slashes, trailing .git; lowercase.
|
|
54
|
+
// (Slashes first so `…/repo.git/` still sheds its .git suffix.)
|
|
55
|
+
s = s.replace(/^([^/:]+):\d+\//, "$1/");
|
|
56
|
+
s = s.replace(/\/+$/, "");
|
|
57
|
+
s = s.replace(/\.git$/i, "");
|
|
58
|
+
return s.toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
async function hkdfBits(ikm, info, bits) {
|
|
61
|
+
const key = await crypto.subtle.importKey("raw", ikm, "HKDF", false, [
|
|
62
|
+
"deriveBits",
|
|
63
|
+
]);
|
|
64
|
+
return crypto.subtle.deriveBits({
|
|
65
|
+
name: "HKDF",
|
|
66
|
+
hash: "SHA-256",
|
|
67
|
+
salt: new TextEncoder().encode(HKDF_SALT),
|
|
68
|
+
info: new TextEncoder().encode(info),
|
|
69
|
+
}, key, bits);
|
|
70
|
+
}
|
|
71
|
+
function hex(buf) {
|
|
72
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
73
|
+
return Array.from(bytes)
|
|
74
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
75
|
+
.join("");
|
|
76
|
+
}
|
|
77
|
+
/** Derive the room id, E2E key, and sender-id key from the repo identity. */
|
|
78
|
+
export async function deriveRoomCredentials(inputs) {
|
|
79
|
+
const ikm = new TextEncoder().encode([
|
|
80
|
+
inputs.rootCommitSha.trim().toLowerCase(),
|
|
81
|
+
normalizeOriginUrl(inputs.originUrl),
|
|
82
|
+
inputs.salt ?? DEFAULT_ROOM_SALT,
|
|
83
|
+
].join("\n"));
|
|
84
|
+
const roomBits = await hkdfBits(ikm, ROOM_INFO, 128);
|
|
85
|
+
const keyBits = await hkdfBits(ikm, KEY_INFO, 256);
|
|
86
|
+
const senderBits = await hkdfBits(ikm, SENDER_INFO, 256);
|
|
87
|
+
const key = await crypto.subtle.importKey("raw", keyBits, { name: "AES-GCM" }, false, [
|
|
88
|
+
"encrypt",
|
|
89
|
+
"decrypt",
|
|
90
|
+
]);
|
|
91
|
+
return { roomId: hex(roomBits), key, senderKeyBytes: new Uint8Array(senderBits) };
|
|
92
|
+
}
|
|
93
|
+
/** Opaque, stable per-machine sender id: HMAC(senderKey, machineLabel). The
|
|
94
|
+
* relay caches last-message-per-sender by this without learning the label. */
|
|
95
|
+
export async function computeSenderId(creds, machineLabel) {
|
|
96
|
+
const hmacKey = await crypto.subtle.importKey("raw", creds.senderKeyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
97
|
+
const sig = await crypto.subtle.sign("HMAC", hmacKey, new TextEncoder().encode(machineLabel));
|
|
98
|
+
return hex(sig).slice(0, 32);
|
|
99
|
+
}
|
|
100
|
+
function toB64(bytes) {
|
|
101
|
+
let bin = "";
|
|
102
|
+
for (const b of bytes)
|
|
103
|
+
bin += String.fromCharCode(b);
|
|
104
|
+
return btoa(bin);
|
|
105
|
+
}
|
|
106
|
+
function fromB64(s) {
|
|
107
|
+
const bin = atob(s);
|
|
108
|
+
const out = new Uint8Array(bin.length);
|
|
109
|
+
for (let i = 0; i < bin.length; i++)
|
|
110
|
+
out[i] = bin.charCodeAt(i);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/** AES-GCM-encrypt a plaintext payload → { iv, ct } base64 pair. */
|
|
114
|
+
export async function encryptPayload(creds, plaintext) {
|
|
115
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
116
|
+
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, creds.key, new TextEncoder().encode(plaintext));
|
|
117
|
+
return { iv: toB64(iv), ct: toB64(new Uint8Array(ct)) };
|
|
118
|
+
}
|
|
119
|
+
/** Decrypt an { iv, ct } pair. Returns null on any failure (wrong room key,
|
|
120
|
+
* tampered frame) — callers drop the frame silently. */
|
|
121
|
+
export async function decryptPayload(creds, frame) {
|
|
122
|
+
try {
|
|
123
|
+
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: fromB64(frame.iv) }, creds.key, fromB64(frame.ct));
|
|
124
|
+
return new TextDecoder().decode(pt);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Hard cap a relay enforces per text frame (ciphertext of a ~2KB blob is far
|
|
131
|
+
* smaller; anything bigger is abuse or a bug). */
|
|
132
|
+
export const MAX_FRAME_BYTES = 16 * 1024;
|
|
133
|
+
/** Parse + shape-validate an incoming relay frame. Null on anything off. */
|
|
134
|
+
export function parseRelayFrame(raw) {
|
|
135
|
+
if (raw.length > MAX_FRAME_BYTES)
|
|
136
|
+
return null;
|
|
137
|
+
let j;
|
|
138
|
+
try {
|
|
139
|
+
j = JSON.parse(raw);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (!j || typeof j !== "object")
|
|
145
|
+
return null;
|
|
146
|
+
const f = j;
|
|
147
|
+
if (f.t === "pub") {
|
|
148
|
+
if (typeof f.sender !== "string" ||
|
|
149
|
+
f.sender.length === 0 ||
|
|
150
|
+
f.sender.length > 64 ||
|
|
151
|
+
typeof f.iv !== "string" ||
|
|
152
|
+
typeof f.ct !== "string" ||
|
|
153
|
+
f.iv.length > 64 ||
|
|
154
|
+
f.ct.length > MAX_FRAME_BYTES) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return { t: "pub", sender: f.sender, iv: f.iv, ct: f.ct };
|
|
158
|
+
}
|
|
159
|
+
if (f.t === "hello") {
|
|
160
|
+
if (typeof f.peers !== "number")
|
|
161
|
+
return null;
|
|
162
|
+
return { t: "hello", peers: f.peers };
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/** Room ids are 32 lowercase hex chars; relays reject anything else. */
|
|
167
|
+
export function isValidRoomId(roomId) {
|
|
168
|
+
return /^[0-9a-f]{32}$/.test(roomId);
|
|
169
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing-mode probe: which auth a headless harness child will actually use.
|
|
3
|
+
*
|
|
4
|
+
* The engine never handles credentials — children are plain harness CLIs and
|
|
5
|
+
* authenticate however that CLI does. But the CLIs prefer an exported API key
|
|
6
|
+
* over a stored (subscription) login when both are present, and that override
|
|
7
|
+
* is almost always an accident: a sourced .env or a leftover export silently
|
|
8
|
+
* moves the run from subsidized subscription billing to per-token API billing.
|
|
9
|
+
* This probe classifies the state per harness so the engine can refuse the
|
|
10
|
+
* silent-override case (see decision 0015 addendum) while leaving deliberate
|
|
11
|
+
* key-only hosts (CI boxes with no login) working.
|
|
12
|
+
*
|
|
13
|
+
* Detection is a heuristic over well-known credential locations; when a
|
|
14
|
+
* location can't prove presence or absence (e.g. Claude Code stores its OAuth
|
|
15
|
+
* token in the macOS keychain, not a file), the state is "unknown" and the
|
|
16
|
+
* engine never hard-fails on it — the harness CLI itself is the final
|
|
17
|
+
* authority and will error loudly if truly unauthenticated.
|
|
18
|
+
*/
|
|
19
|
+
import type { HarnessName } from "./types.js";
|
|
20
|
+
/** The env var each harness CLI reads as an API key (preferred over a stored
|
|
21
|
+
* login when set). Shared with the child-env builder's subscription-only
|
|
22
|
+
* scrub. */
|
|
23
|
+
export declare const API_KEY_VARS: Record<string, string>;
|
|
24
|
+
export type LoginState = "present" | "absent" | "unknown";
|
|
25
|
+
export type BillingMode =
|
|
26
|
+
/** No API key exported; the child rides the stored (subscription) login. */
|
|
27
|
+
"subscription"
|
|
28
|
+
/** API key present, no stored login detected: a deliberate key-only host. */
|
|
29
|
+
| "api-key"
|
|
30
|
+
/** API key present AND a stored login exists: the key silently overrides
|
|
31
|
+
* subsidized auth. The engine refuses this unless explicitly allowed. */
|
|
32
|
+
| "api-key-override";
|
|
33
|
+
export interface BillingProbe {
|
|
34
|
+
harness: HarnessName;
|
|
35
|
+
/** The env var checked (from API_KEY_VARS), or a note when the key was
|
|
36
|
+
* found in a credential file instead of the environment. */
|
|
37
|
+
apiKeySource: string | null;
|
|
38
|
+
apiKeyPresent: boolean;
|
|
39
|
+
login: LoginState;
|
|
40
|
+
mode: BillingMode;
|
|
41
|
+
}
|
|
42
|
+
export interface ProbeIo {
|
|
43
|
+
env?: NodeJS.ProcessEnv;
|
|
44
|
+
home?: string;
|
|
45
|
+
}
|
|
46
|
+
export type BillingProber = (harness: HarnessName) => BillingProbe;
|
|
47
|
+
export declare function probeBilling(harness: HarnessName, io?: ProbeIo): BillingProbe;
|
|
48
|
+
//# sourceMappingURL=billing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/billing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;YAEY;AACZ,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAI/C,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,WAAW;AACrB,4EAA4E;AAC1E,cAAc;AAChB,6EAA6E;GAC3E,SAAS;AACX;yEACyE;GACvE,kBAAkB,CAAC;AAEvB,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,WAAW,CAAC;IACrB;gEAC4D;IAC5D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,YAAY,CAAC;AAEnE,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,EAAE,GAAE,OAAY,GAAG,YAAY,CA+CjF"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing-mode probe: which auth a headless harness child will actually use.
|
|
3
|
+
*
|
|
4
|
+
* The engine never handles credentials — children are plain harness CLIs and
|
|
5
|
+
* authenticate however that CLI does. But the CLIs prefer an exported API key
|
|
6
|
+
* over a stored (subscription) login when both are present, and that override
|
|
7
|
+
* is almost always an accident: a sourced .env or a leftover export silently
|
|
8
|
+
* moves the run from subsidized subscription billing to per-token API billing.
|
|
9
|
+
* This probe classifies the state per harness so the engine can refuse the
|
|
10
|
+
* silent-override case (see decision 0015 addendum) while leaving deliberate
|
|
11
|
+
* key-only hosts (CI boxes with no login) working.
|
|
12
|
+
*
|
|
13
|
+
* Detection is a heuristic over well-known credential locations; when a
|
|
14
|
+
* location can't prove presence or absence (e.g. Claude Code stores its OAuth
|
|
15
|
+
* token in the macOS keychain, not a file), the state is "unknown" and the
|
|
16
|
+
* engine never hard-fails on it — the harness CLI itself is the final
|
|
17
|
+
* authority and will error loudly if truly unauthenticated.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
/** The env var each harness CLI reads as an API key (preferred over a stored
|
|
23
|
+
* login when set). Shared with the child-env builder's subscription-only
|
|
24
|
+
* scrub. */
|
|
25
|
+
export const API_KEY_VARS = {
|
|
26
|
+
"claude-code": "ANTHROPIC_API_KEY",
|
|
27
|
+
codex: "OPENAI_API_KEY",
|
|
28
|
+
cursor: "CURSOR_API_KEY",
|
|
29
|
+
};
|
|
30
|
+
export function probeBilling(harness, io = {}) {
|
|
31
|
+
const env = io.env ?? process.env;
|
|
32
|
+
const home = io.home ?? homedir();
|
|
33
|
+
const keyVar = API_KEY_VARS[harness];
|
|
34
|
+
const envKey = keyVar ? Boolean(env[keyVar]?.trim()) : false;
|
|
35
|
+
let login;
|
|
36
|
+
let apiKeyPresent = envKey;
|
|
37
|
+
let apiKeySource = envKey ? keyVar : null;
|
|
38
|
+
switch (harness) {
|
|
39
|
+
case "claude-code":
|
|
40
|
+
login = probeClaudeLogin(home);
|
|
41
|
+
break;
|
|
42
|
+
case "codex": {
|
|
43
|
+
const codex = probeCodexAuth(env, home);
|
|
44
|
+
login = codex.login;
|
|
45
|
+
// `codex login --api-key` stores the key in auth.json rather than the
|
|
46
|
+
// env; that is still API-key billing and should surface as such.
|
|
47
|
+
if (!envKey && codex.storedApiKey) {
|
|
48
|
+
apiKeyPresent = true;
|
|
49
|
+
apiKeySource = codex.storedApiKeyNote;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "cursor":
|
|
54
|
+
// cursor-agent's stored-login location is not yet verified against a
|
|
55
|
+
// live install (adapter itself is pending verification); never claim
|
|
56
|
+
// presence or absence we can't prove.
|
|
57
|
+
login = "unknown";
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
// External adapters own their authentication. The generic engine must
|
|
61
|
+
// not guess credential locations or fail a plugin on a built-in-only
|
|
62
|
+
// heuristic.
|
|
63
|
+
login = "unknown";
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const mode = apiKeyPresent && login === "present"
|
|
67
|
+
? "api-key-override"
|
|
68
|
+
: apiKeyPresent
|
|
69
|
+
? "api-key"
|
|
70
|
+
: "subscription";
|
|
71
|
+
return { harness, apiKeySource, apiKeyPresent, login, mode };
|
|
72
|
+
}
|
|
73
|
+
/** Claude Code: OAuth login lives at ~/.claude/.credentials.json on Linux;
|
|
74
|
+
* macOS stores it in the keychain (dir exists, file doesn't → unknown). */
|
|
75
|
+
function probeClaudeLogin(home) {
|
|
76
|
+
const dir = join(home, ".claude");
|
|
77
|
+
const credFile = join(dir, ".credentials.json");
|
|
78
|
+
if (existsSync(credFile)) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(readFileSync(credFile, "utf8"));
|
|
81
|
+
return parsed.claudeAiOauth?.accessToken ? "present" : "absent";
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return "unknown";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return existsSync(dir) ? "unknown" : "absent";
|
|
88
|
+
}
|
|
89
|
+
/** codex: auth lives at $CODEX_HOME/auth.json (default ~/.codex). A `tokens`
|
|
90
|
+
* object means a ChatGPT (subscription) login; an OPENAI_API_KEY field means
|
|
91
|
+
* a stored API key (`codex login --api-key`). */
|
|
92
|
+
function probeCodexAuth(env, home) {
|
|
93
|
+
const authPath = join(env.CODEX_HOME?.trim() || join(home, ".codex"), "auth.json");
|
|
94
|
+
const storedApiKeyNote = `${authPath} (stored key)`;
|
|
95
|
+
if (!existsSync(authPath))
|
|
96
|
+
return { login: "absent", storedApiKey: false, storedApiKeyNote };
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(readFileSync(authPath, "utf8"));
|
|
99
|
+
return {
|
|
100
|
+
login: parsed.tokens ? "present" : "absent",
|
|
101
|
+
storedApiKey: !parsed.tokens && Boolean(parsed.OPENAI_API_KEY),
|
|
102
|
+
storedApiKeyNote,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { login: "unknown", storedApiKey: false, storedApiKeyNote };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-process environment builder shared by every spawn adapter.
|
|
3
|
+
*
|
|
4
|
+
* Rules (the first is a hard-won Phase 1 spike finding):
|
|
5
|
+
* 1. **Delete inherited harness-session vars, never blank them.** A nested
|
|
6
|
+
* harness CLI under a live session inherits vars that make it exit 1 with
|
|
7
|
+
* empty output; an empty-string var still reads as set. Scrub all three
|
|
8
|
+
* families (CLAUDE*, CODEX*, CURSOR*) regardless of which adapter spawns —
|
|
9
|
+
* a codex child launched from inside a Claude Code session must not
|
|
10
|
+
* inherit CLAUDE* either.
|
|
11
|
+
* 2. **The scrub targets SESSION vars, not credentials.** CURSOR_API_KEY
|
|
12
|
+
* matches the CURSOR* prefix but is an auth credential, so it is carved
|
|
13
|
+
* out and re-added — a key-only cursor host must keep working. (The other
|
|
14
|
+
* two key vars, ANTHROPIC_API_KEY and OPENAI_API_KEY, don't collide with
|
|
15
|
+
* the scrub prefixes.)
|
|
16
|
+
* 3. **subscriptionOnly deletes every API-key var** (see billing.ts) so the
|
|
17
|
+
* child can only authenticate via its stored subscription login — the
|
|
18
|
+
* guarantee behind `workflow run --subscription-only`.
|
|
19
|
+
* 4. **Mark the child as a workflow child** (HARNERY_WORKFLOW_CHILD=1): the
|
|
20
|
+
* stop-hook rule exempts it from the human-facing end-of-turn ritual while
|
|
21
|
+
* hooks stay on, keeping heartbeat + event capture.
|
|
22
|
+
* 5. **Stamp the run id** (HARNERY_WORKFLOW_RUN_ID) so the coord layer and
|
|
23
|
+
* web UI can associate child sessions with their workflow run.
|
|
24
|
+
*/
|
|
25
|
+
export interface ChildEnvOpts {
|
|
26
|
+
/** Delete all API-key vars so children can only use stored logins. */
|
|
27
|
+
subscriptionOnly?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function buildChildEnv(runId?: string, opts?: ChildEnvOpts): Record<string, string>;
|
|
30
|
+
//# sourceMappingURL=child-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"child-env.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/child-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAY7F"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-process environment builder shared by every spawn adapter.
|
|
3
|
+
*
|
|
4
|
+
* Rules (the first is a hard-won Phase 1 spike finding):
|
|
5
|
+
* 1. **Delete inherited harness-session vars, never blank them.** A nested
|
|
6
|
+
* harness CLI under a live session inherits vars that make it exit 1 with
|
|
7
|
+
* empty output; an empty-string var still reads as set. Scrub all three
|
|
8
|
+
* families (CLAUDE*, CODEX*, CURSOR*) regardless of which adapter spawns —
|
|
9
|
+
* a codex child launched from inside a Claude Code session must not
|
|
10
|
+
* inherit CLAUDE* either.
|
|
11
|
+
* 2. **The scrub targets SESSION vars, not credentials.** CURSOR_API_KEY
|
|
12
|
+
* matches the CURSOR* prefix but is an auth credential, so it is carved
|
|
13
|
+
* out and re-added — a key-only cursor host must keep working. (The other
|
|
14
|
+
* two key vars, ANTHROPIC_API_KEY and OPENAI_API_KEY, don't collide with
|
|
15
|
+
* the scrub prefixes.)
|
|
16
|
+
* 3. **subscriptionOnly deletes every API-key var** (see billing.ts) so the
|
|
17
|
+
* child can only authenticate via its stored subscription login — the
|
|
18
|
+
* guarantee behind `workflow run --subscription-only`.
|
|
19
|
+
* 4. **Mark the child as a workflow child** (HARNERY_WORKFLOW_CHILD=1): the
|
|
20
|
+
* stop-hook rule exempts it from the human-facing end-of-turn ritual while
|
|
21
|
+
* hooks stay on, keeping heartbeat + event capture.
|
|
22
|
+
* 5. **Stamp the run id** (HARNERY_WORKFLOW_RUN_ID) so the coord layer and
|
|
23
|
+
* web UI can associate child sessions with their workflow run.
|
|
24
|
+
*/
|
|
25
|
+
import { API_KEY_VARS } from "./billing.js";
|
|
26
|
+
const SCRUB_PREFIXES = ["CLAUDE", "CODEX", "CURSOR"];
|
|
27
|
+
export function buildChildEnv(runId, opts = {}) {
|
|
28
|
+
const env = {};
|
|
29
|
+
const keyVars = new Set(Object.values(API_KEY_VARS));
|
|
30
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
31
|
+
if (v === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
if (opts.subscriptionOnly && keyVars.has(k))
|
|
34
|
+
continue;
|
|
35
|
+
if (SCRUB_PREFIXES.some((p) => k.startsWith(p)) && !keyVars.has(k))
|
|
36
|
+
continue;
|
|
37
|
+
env[k] = v;
|
|
38
|
+
}
|
|
39
|
+
env.HARNERY_WORKFLOW_CHILD = "1";
|
|
40
|
+
if (runId)
|
|
41
|
+
env.HARNERY_WORKFLOW_RUN_ID = runId;
|
|
42
|
+
return env;
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow engine: loads a workflow script (plain JS, `export default
|
|
3
|
+
* async (ctx) => …`), injects the ctx API, enforces the caps, journals every
|
|
4
|
+
* step to `.harnery/workflows/<run-id>/journal.jsonl`, and returns a RunReport.
|
|
5
|
+
*
|
|
6
|
+
* Guarantees the engine makes (the pitch, in code):
|
|
7
|
+
* - **Bounded**: hard total-agent ceiling + bounded parallel() concurrency.
|
|
8
|
+
* A runaway loop hits `maxAgents` and the run fails loud, not silently.
|
|
9
|
+
* - **Terminating**: the run is over when the script's default export
|
|
10
|
+
* returns. There is no recursive self-spawning path: subagents are leaf
|
|
11
|
+
* processes; only the top-level script can spawn.
|
|
12
|
+
* - **Schema-gated**: with `schema`, an agent's reply must strict-parse and
|
|
13
|
+
* validate; failures re-prompt with the validation errors appended, up to
|
|
14
|
+
* `maxAttempts`, then throw. Routing decisions read validated fields, so
|
|
15
|
+
* the deterministic script — not a model — decides what runs next.
|
|
16
|
+
* - **Journaled**: every stage/agent start+end lands in the run journal with
|
|
17
|
+
* cost, duration, and child session id (the resume + web-UI substrate).
|
|
18
|
+
*/
|
|
19
|
+
import type { EngineOpts, RunReport } from "./types.js";
|
|
20
|
+
export declare function runWorkflow(scriptPath: string, opts: EngineOpts): Promise<RunReport>;
|
|
21
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,KAAK,EAEV,UAAU,EAEV,SAAS,EAIV,MAAM,YAAY,CAAC;AASpB,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAgS1F"}
|