harnery 0.14.0 → 0.16.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 +1 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/supervisor.d.ts +4 -0
- package/dist/commands/supervisor.d.ts.map +1 -0
- package/dist/commands/supervisor.js +446 -0
- package/dist/core/supervisor/index.d.ts +4 -0
- package/dist/core/supervisor/index.d.ts.map +1 -0
- package/dist/core/supervisor/index.js +3 -0
- package/dist/core/supervisor/read.d.ts +4 -0
- package/dist/core/supervisor/read.d.ts.map +1 -0
- package/dist/core/supervisor/read.js +2 -0
- package/dist/core/supervisor/runner.d.ts +34 -0
- package/dist/core/supervisor/runner.d.ts.map +1 -0
- package/dist/core/supervisor/runner.js +198 -0
- package/dist/core/supervisor/service-read.d.ts +6 -0
- package/dist/core/supervisor/service-read.d.ts.map +1 -0
- package/dist/core/supervisor/service-read.js +127 -0
- package/dist/core/supervisor/service.d.ts +114 -0
- package/dist/core/supervisor/service.d.ts.map +1 -0
- package/dist/core/supervisor/service.js +699 -0
- package/dist/core/supervisor/state.d.ts +69 -0
- package/dist/core/supervisor/state.d.ts.map +1 -0
- package/dist/core/supervisor/state.js +413 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +21 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +1 -0
- package/dist/core/workflow/run-state.d.ts +2 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +12 -0
- package/dist/core/workflow/specialists.d.ts +7 -0
- package/dist/core/workflow/specialists.d.ts.map +1 -0
- package/dist/core/workflow/specialists.js +78 -0
- package/dist/core/workflow/types.d.ts +17 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.js +6 -1
- package/package.json +11 -1
- package/src/commander.ts +2 -0
- package/src/commands/supervisor.ts +575 -0
- package/src/core/supervisor/index.ts +51 -0
- package/src/core/supervisor/read.ts +29 -0
- package/src/core/supervisor/runner.ts +278 -0
- package/src/core/supervisor/service-read.ts +144 -0
- package/src/core/supervisor/service.ts +920 -0
- package/src/core/supervisor/state.ts +552 -0
- package/src/core/workflow/engine.ts +21 -8
- package/src/core/workflow/index.ts +1 -0
- package/src/core/workflow/proof.ts +1 -0
- package/src/core/workflow/run-state.ts +17 -0
- package/src/core/workflow/specialists.ts +97 -0
- package/src/core/workflow/types.ts +18 -0
- package/src/lib/tunnel/gate.ts +7 -1
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
policyDigest,
|
|
23
23
|
} from "../policy/index.ts";
|
|
24
24
|
import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.ts";
|
|
25
|
+
import { normalizeWorkflowSpecialists } from "./specialists.ts";
|
|
26
|
+
import type { WorkflowSpecialistProfile } from "./types.ts";
|
|
25
27
|
|
|
26
28
|
export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 1 as const;
|
|
27
29
|
|
|
@@ -48,6 +50,7 @@ export interface WorkflowRunManifest {
|
|
|
48
50
|
isolation: PolicyIsolation;
|
|
49
51
|
network_access: PolicyNetworkAccess;
|
|
50
52
|
policy?: NormalizedPolicy;
|
|
53
|
+
specialists?: Record<string, WorkflowSpecialistProfile>;
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -253,10 +256,24 @@ function validExecution(value: WorkflowRunManifest["execution"]): boolean {
|
|
|
253
256
|
value.approval_addressee.length <= 200 &&
|
|
254
257
|
["shared", "worktree", "sandbox", "remote"].includes(value.isolation) &&
|
|
255
258
|
["enabled", "disabled", "unknown"].includes(value.network_access) &&
|
|
259
|
+
validSpecialists(value.specialists) &&
|
|
256
260
|
validFrozenPolicy(value.policy, value.cwd)
|
|
257
261
|
);
|
|
258
262
|
}
|
|
259
263
|
|
|
264
|
+
function validSpecialists(
|
|
265
|
+
specialists: Record<string, WorkflowSpecialistProfile> | undefined,
|
|
266
|
+
): boolean {
|
|
267
|
+
if (specialists === undefined) return true;
|
|
268
|
+
try {
|
|
269
|
+
return (
|
|
270
|
+
JSON.stringify(normalizeWorkflowSpecialists(specialists)) === JSON.stringify(specialists)
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
260
277
|
function validFrozenPolicy(policy: NormalizedPolicy | undefined, cwd: string): boolean {
|
|
261
278
|
if (policy === undefined) return true;
|
|
262
279
|
try {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AgentOpts, WorkflowSpecialistProfile } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const SPECIALIST_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
4
|
+
const MAX_SPECIALISTS = 32;
|
|
5
|
+
const MAX_INSTRUCTIONS = 4_000;
|
|
6
|
+
const MAX_OPTION = 200;
|
|
7
|
+
|
|
8
|
+
export function normalizeWorkflowSpecialists(
|
|
9
|
+
input: Readonly<Record<string, WorkflowSpecialistProfile>> | undefined,
|
|
10
|
+
): Record<string, WorkflowSpecialistProfile> {
|
|
11
|
+
if (input === undefined) return {};
|
|
12
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
13
|
+
throw new Error("workflow specialists must be an object keyed by role id");
|
|
14
|
+
}
|
|
15
|
+
const entries = Object.entries(input);
|
|
16
|
+
if (entries.length > MAX_SPECIALISTS) {
|
|
17
|
+
throw new Error(`workflow specialists exceed ${MAX_SPECIALISTS} roles`);
|
|
18
|
+
}
|
|
19
|
+
const normalized: Record<string, WorkflowSpecialistProfile> = {};
|
|
20
|
+
for (const [id, profile] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
21
|
+
if (!SPECIALIST_ID.test(id))
|
|
22
|
+
throw new Error(`invalid workflow specialist id ${JSON.stringify(id)}`);
|
|
23
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
|
|
24
|
+
throw new Error(`workflow specialist ${id} must be an object`);
|
|
25
|
+
}
|
|
26
|
+
normalized[id] = {
|
|
27
|
+
instructions: bounded(
|
|
28
|
+
profile.instructions,
|
|
29
|
+
`workflow specialist ${id} instructions`,
|
|
30
|
+
MAX_INSTRUCTIONS,
|
|
31
|
+
),
|
|
32
|
+
harness: optional(profile.harness, `workflow specialist ${id} harness`),
|
|
33
|
+
model: optional(profile.model, `workflow specialist ${id} model`),
|
|
34
|
+
effort: optional(profile.effort, `workflow specialist ${id} effort`),
|
|
35
|
+
maxAttempts: positiveOptional(
|
|
36
|
+
profile.maxAttempts,
|
|
37
|
+
`workflow specialist ${id} maxAttempts`,
|
|
38
|
+
10,
|
|
39
|
+
),
|
|
40
|
+
timeoutMs: positiveOptional(
|
|
41
|
+
profile.timeoutMs,
|
|
42
|
+
`workflow specialist ${id} timeoutMs`,
|
|
43
|
+
24 * 60 * 60 * 1_000,
|
|
44
|
+
),
|
|
45
|
+
maxTurns: positiveOptional(profile.maxTurns, `workflow specialist ${id} maxTurns`, 1_000),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveSpecialistAssignment(
|
|
52
|
+
profiles: Readonly<Record<string, WorkflowSpecialistProfile>>,
|
|
53
|
+
prompt: string,
|
|
54
|
+
opts: AgentOpts,
|
|
55
|
+
): { prompt: string; opts: AgentOpts } {
|
|
56
|
+
if (!opts.specialist) return { prompt, opts };
|
|
57
|
+
if (!SPECIALIST_ID.test(opts.specialist)) {
|
|
58
|
+
throw new Error(`invalid workflow specialist id ${JSON.stringify(opts.specialist)}`);
|
|
59
|
+
}
|
|
60
|
+
const profile = profiles[opts.specialist];
|
|
61
|
+
if (!profile)
|
|
62
|
+
throw new Error(`workflow specialist ${JSON.stringify(opts.specialist)} is not configured`);
|
|
63
|
+
return {
|
|
64
|
+
prompt: `${profile.instructions}\n\nAssignment:\n${prompt}`,
|
|
65
|
+
opts: {
|
|
66
|
+
specialist: opts.specialist,
|
|
67
|
+
harness: opts.harness ?? profile.harness,
|
|
68
|
+
model: opts.model ?? profile.model,
|
|
69
|
+
effort: opts.effort ?? profile.effort,
|
|
70
|
+
maxAttempts: opts.maxAttempts ?? profile.maxAttempts,
|
|
71
|
+
timeoutMs: opts.timeoutMs ?? profile.timeoutMs,
|
|
72
|
+
maxTurns: opts.maxTurns ?? profile.maxTurns,
|
|
73
|
+
label: opts.label,
|
|
74
|
+
schema: opts.schema,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function bounded(value: unknown, field: string, max: number): string {
|
|
80
|
+
if (typeof value !== "string") throw new Error(`${field} must be a string`);
|
|
81
|
+
const normalized = value.trim();
|
|
82
|
+
if (!normalized) throw new Error(`${field} must not be empty`);
|
|
83
|
+
if (normalized.length > max) throw new Error(`${field} exceeds ${max} characters`);
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function optional(value: unknown, field: string): string | undefined {
|
|
88
|
+
return value === undefined ? undefined : bounded(value, field, MAX_OPTION);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function positiveOptional(value: unknown, field: string, max: number): number | undefined {
|
|
92
|
+
if (value === undefined) return undefined;
|
|
93
|
+
if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > max) {
|
|
94
|
+
throw new Error(`${field} must be an integer from 1 to ${max}`);
|
|
95
|
+
}
|
|
96
|
+
return value as number;
|
|
97
|
+
}
|
|
@@ -82,6 +82,7 @@ export interface WorkflowAgentProof {
|
|
|
82
82
|
id: string;
|
|
83
83
|
label: string;
|
|
84
84
|
stage?: string;
|
|
85
|
+
specialist?: string;
|
|
85
86
|
harness: HarnessName;
|
|
86
87
|
model?: string;
|
|
87
88
|
status: "succeeded" | "failed" | "cached";
|
|
@@ -210,6 +211,9 @@ export interface StageSchema {
|
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
export interface AgentOpts {
|
|
214
|
+
/** Frozen specialist profile whose instructions and defaults wrap this
|
|
215
|
+
* assignment. The host supplies profiles through EngineOpts.specialists. */
|
|
216
|
+
specialist?: string;
|
|
213
217
|
/** Stage gate: when present, the agent's reply must strict-parse as JSON and
|
|
214
218
|
* validate; the engine retries with the validation error appended, up to
|
|
215
219
|
* `maxAttempts`. Without it, `agent()` resolves to the raw reply text. */
|
|
@@ -239,6 +243,18 @@ export interface AgentOpts {
|
|
|
239
243
|
* a package-owned union first. */
|
|
240
244
|
export type HarnessName = string;
|
|
241
245
|
|
|
246
|
+
/** Durable role defaults supplied by a goal supervisor or embedding host.
|
|
247
|
+
* Profiles are frozen into a workflow run manifest before the first spawn. */
|
|
248
|
+
export interface WorkflowSpecialistProfile {
|
|
249
|
+
instructions: string;
|
|
250
|
+
harness?: HarnessName;
|
|
251
|
+
model?: string;
|
|
252
|
+
effort?: string;
|
|
253
|
+
maxAttempts?: number;
|
|
254
|
+
timeoutMs?: number;
|
|
255
|
+
maxTurns?: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
242
258
|
/** What a spawn adapter returns for one subagent run. */
|
|
243
259
|
export interface SpawnResult {
|
|
244
260
|
ok: boolean;
|
|
@@ -309,6 +325,8 @@ export interface EngineOpts {
|
|
|
309
325
|
spawners: Readonly<Record<HarnessName, Spawner | undefined>>;
|
|
310
326
|
/** Harness used when an agent() call doesn't name one (default "claude-code"). */
|
|
311
327
|
defaultHarness?: HarnessName;
|
|
328
|
+
/** Named specialist roles available to agent(..., { specialist }). */
|
|
329
|
+
specialists?: Readonly<Record<string, WorkflowSpecialistProfile>>;
|
|
312
330
|
/** Resume: run id of a prior run whose journal supplies cached results.
|
|
313
331
|
* agent() calls whose (stage, prompt, model, maxTurns, schema) key matches a
|
|
314
332
|
* completed prior agent return the journaled result without spawning. */
|
package/src/lib/tunnel/gate.ts
CHANGED
|
@@ -47,13 +47,19 @@ const server = Bun.serve<WsData, never>({
|
|
|
47
47
|
// Bun's max, and beyond any realistic single-turn delay.
|
|
48
48
|
idleTimeout: 255,
|
|
49
49
|
async fetch(req, server) {
|
|
50
|
+
const url = new URL(req.url);
|
|
50
51
|
if (ACCESS === "cloudflare-allowlist") {
|
|
51
52
|
const ip = req.headers.get("cf-connecting-ip") ?? "";
|
|
52
53
|
if (!ALLOW.has(ip)) {
|
|
54
|
+
// Log denials so operators can whitelist a phone/laptop that just
|
|
55
|
+
// hit 403 without asking the human to dig up their public IP.
|
|
56
|
+
console.log(
|
|
57
|
+
// lint-ok-emission: detached worker, see file note above
|
|
58
|
+
`deny: ${ip || "(missing-cf-connecting-ip)"} ${req.method} ${url.pathname}`,
|
|
59
|
+
);
|
|
53
60
|
return new Response("403 Forbidden\n", { status: 403 });
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
|
-
const url = new URL(req.url);
|
|
57
63
|
|
|
58
64
|
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
59
65
|
if (server.upgrade(req, { data: { path: url.pathname + url.search } })) {
|