@code-yeongyu/senpi 2026.11.6-1 → 2026.11.6-2
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/CHANGELOG.md +35 -0
- package/dist/bundle/chunks/{chunk-5MWHZH2O.js → chunk-2FDW2LQL.js} +1 -1
- package/dist/bundle/chunks/{chunk-GG2JO3CL.js → chunk-4E7GDG4H.js} +1 -1
- package/dist/bundle/chunks/{chunk-K5JLYACP.js → chunk-4GKU755E.js} +1 -1
- package/dist/bundle/chunks/{chunk-437VLZQZ.js → chunk-7III44PV.js} +1 -1
- package/dist/bundle/chunks/chunk-7PSBD5KS.js +9 -0
- package/dist/bundle/chunks/{chunk-KFWHLVVH.js → chunk-C7OKUC7L.js} +1 -1
- package/dist/bundle/chunks/{chunk-CX32K5KD.js → chunk-CF5OGBC4.js} +7 -7
- package/dist/bundle/chunks/{chunk-7TGZJH6Z.js → chunk-D5MRJWUK.js} +40 -39
- package/dist/bundle/chunks/{chunk-E6L5VZSM.js → chunk-DSWDJ7HT.js} +1 -1
- package/dist/bundle/chunks/{chunk-6NMTFURP.js → chunk-ECW7VBMB.js} +1 -1
- package/dist/bundle/chunks/{chunk-2CSLJLM6.js → chunk-GKAYPJ4E.js} +2 -2
- package/dist/bundle/chunks/{chunk-KCGUUIUS.js → chunk-GNSMNSL7.js} +1 -1
- package/dist/bundle/chunks/{chunk-V3JSQ3PG.js → chunk-GSKCWXXG.js} +1 -1
- package/dist/bundle/chunks/{chunk-USKWGPA6.js → chunk-GTJJ73DE.js} +1 -1
- package/dist/bundle/chunks/{chunk-HHSYTDKO.js → chunk-HLM46XEB.js} +2 -2
- package/dist/bundle/chunks/{chunk-A4F4J2KH.js → chunk-I3PHVJAS.js} +2 -2
- package/dist/bundle/chunks/{chunk-LAMRXFHZ.js → chunk-LNPAT22L.js} +1 -1
- package/dist/bundle/chunks/chunk-NH7SYIGN.js +2 -0
- package/dist/bundle/chunks/{chunk-ZIIDIVTG.js → chunk-R2Z7CX77.js} +1 -1
- package/dist/bundle/chunks/{chunk-32I42CEY.js → chunk-RQ6CHXQ2.js} +1 -1
- package/dist/bundle/chunks/chunk-SJI6UB23.js +2 -0
- package/dist/bundle/chunks/{chunk-KUIEHAPR.js → chunk-T2SMQPRE.js} +1 -1
- package/dist/bundle/chunks/{chunk-IPG434JT.js → chunk-TIPKAMSS.js} +1 -1
- package/dist/bundle/chunks/{chunk-RI5ABX7A.js → chunk-TMXRR3N7.js} +2 -2
- package/dist/bundle/chunks/chunk-VD7AKTGP.js +3 -0
- package/dist/bundle/chunks/{chunk-22JICYKS.js → chunk-WAOGBBZR.js} +1 -1
- package/dist/bundle/chunks/{chunk-6MHCFJDB.js → chunk-WGRKM3N6.js} +2 -2
- package/dist/bundle/chunks/{chunk-WF5R2VSP.js → chunk-X5ZVYNOL.js} +1 -1
- package/dist/bundle/chunks/{chunk-H2FBGZNP.js → chunk-XCMII2VQ.js} +2 -2
- package/dist/bundle/chunks/{chunk-5JUCTKYS.js → chunk-YK3ZZCHI.js} +1 -1
- package/dist/bundle/chunks/{cli-main-ZQBSX5H5.js → cli-main-T74YJGIF.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-7U7354GG.js → hand-entry-AYRUGE2A.js} +2 -2
- package/dist/bundle/chunks/{headless-host-7LKTG76U.js → headless-host-ES74ZB6M.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-NLSWH2CA.js → help-fast-path-TMTGLOBG.js} +1 -1
- package/dist/bundle/chunks/{host-command-3YRFN35E.js → host-command-YRXZE4TV.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-6XLVIECJ.js → host-lifecycle-6VCXMPDW.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-F7USXUZV.js → host-session-command-N7IN7ZDV.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-4RF3PUQ2.js → interactive-mode-B5SQDLH7.js} +1 -1
- package/dist/bundle/chunks/{list-tips-H6VGDFTS.js → list-tips-DU57HHKA.js} +1 -1
- package/dist/bundle/chunks/{main-G2SRZYQN.js → main-UQFVFCNY.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-RPN54GUC.js → multi-session-host-XSUJDHFZ.js} +5 -5
- package/dist/bundle/chunks/{openai-codex-responses-MJZKUS6I.js → openai-codex-responses-7V6LTDRU.js} +2 -2
- package/dist/bundle/chunks/{package-manager-cli-D2VFD463.js → package-manager-cli-GLUNPQJU.js} +1 -1
- package/dist/bundle/chunks/{role-37WVG42T.js → role-RRFFHZF5.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-QELPMM4N.js → rotation-stream-BN2YK2MC.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-P4D3DVYD.js → rpc-mode-JYN3E7P7.js} +1 -1
- package/dist/bundle/chunks/rpc-monitor-source-C6VQ3BC4.js +3 -0
- package/dist/bundle/chunks/{run-attached-tui-CGA6O5FR.js → run-attached-tui-W236SEOR.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-XWSNOAGG.js → schedule-command-B2EJTBOE.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-YIMLHGLG.js +3 -0
- package/dist/bundle/chunks/{session-picker-OA2AUVZG.js → session-picker-E6ZONUJB.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +294 -292
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/cli/project-trust-headless.d.ts +13 -0
- package/dist/cli/project-trust-headless.js +35 -0
- package/dist/cli/project-trust.js +13 -50
- package/dist/cli/schedule-delivery.d.ts +4 -0
- package/dist/cli/schedule-delivery.js +6 -0
- package/dist/cli/schedule-runner.js +8 -0
- package/dist/cli/session-opening.d.ts +19 -0
- package/dist/cli/session-opening.js +47 -1
- package/dist/core/agent-session-runtime.d.ts +12 -0
- package/dist/core/agent-session-runtime.js +41 -0
- package/dist/core/agent-session.js +13 -22
- package/dist/core/egress/api-table.d.ts +121 -0
- package/dist/core/egress/api-table.js +41 -0
- package/dist/core/egress/classify.d.ts +19 -0
- package/dist/core/egress/classify.js +197 -0
- package/dist/core/egress/credential-fields.d.ts +18 -0
- package/dist/core/egress/credential-fields.js +83 -0
- package/dist/core/egress/custody-sidecar.d.ts +44 -0
- package/dist/core/egress/custody-sidecar.js +91 -0
- package/dist/core/egress/device-custody.d.ts +12 -0
- package/dist/core/egress/device-custody.js +82 -0
- package/dist/core/egress/endpoint.d.ts +33 -0
- package/dist/core/egress/endpoint.js +115 -0
- package/dist/core/execute-tool-on-host.d.ts +26 -0
- package/dist/core/execute-tool-on-host.js +96 -0
- package/dist/core/extensions/builtin/compaction/retained-message-safety.js +15 -0
- package/dist/core/extensions/builtin/goal/index.js +15 -3
- package/dist/core/extensions/builtin/goal/stop-pause.d.ts +2 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +5 -0
- package/dist/core/extensions/builtin/herdr/herdr-state.d.ts +10 -0
- package/dist/core/extensions/builtin/herdr/herdr-state.js +34 -6
- package/dist/core/extensions/builtin/herdr/index.js +6 -2
- package/dist/core/extensions/builtin/mcp/service.d.ts +9 -1
- package/dist/core/extensions/builtin/mcp/service.js +32 -1
- package/dist/core/extensions/builtin/terminal/rpc-monitor-remote.d.ts +43 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-remote.js +158 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-render.d.ts +2 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-render.js +4 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-source.d.ts +5 -2
- package/dist/core/extensions/builtin/terminal/rpc-monitor-source.js +19 -4
- package/dist/core/extensions/builtin/terminal/rpc-monitor-targets.d.ts +4 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-targets.js +15 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-watch.d.ts +10 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-watch.js +28 -2
- package/dist/core/extensions/builtin/terminal/rpc-remote-source.d.ts +59 -0
- package/dist/core/extensions/builtin/terminal/rpc-remote-source.js +15 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +5 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +14 -1
- package/dist/core/extensions/builtin/terminal/tools/monitor-rpc.js +8 -1
- package/dist/core/extensions/loader.js +8 -0
- package/dist/core/extensions/types.d.ts +6 -0
- package/dist/core/foreign-session-holders.d.ts +1 -1
- package/dist/core/foreign-session-holders.js +3 -3
- package/dist/core/session-import/jsonl-import.d.ts +8 -12
- package/dist/core/session-import/jsonl-import.js +99 -40
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.js +7 -2
- package/dist/core/session-store/durable-binding.d.ts +2 -0
- package/dist/core/session-store/durable-binding.js +26 -4
- package/dist/core/settings-manager.d.ts +20 -0
- package/dist/core/settings-manager.js +51 -0
- package/dist/core/trust-manager.d.ts +1 -18
- package/dist/core/trust-manager.js +3 -124
- package/dist/core/trust-requiring-resources.d.ts +21 -0
- package/dist/core/trust-requiring-resources.js +189 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/main.js +42 -12
- package/dist/modes/interactive/interactive-mode.js +2 -2
- package/dist/modes/interactive/session-control-commands.js +2 -0
- package/dist/modes/rpc/connection-handler.js +17 -0
- package/dist/modes/rpc/connection-question-bridge.js +7 -4
- package/dist/modes/rpc/effect-commands.d.ts +24 -0
- package/dist/modes/rpc/host-boot-extensions.d.ts +4 -0
- package/dist/modes/rpc/host-boot-extensions.js +17 -0
- package/dist/modes/rpc/host-lifecycle-shutdown.js +11 -2
- package/dist/modes/rpc/loopless-runtime-settings.js +5 -1
- package/dist/modes/rpc/method-classes-schema-session.d.ts +33 -0
- package/dist/modes/rpc/method-classes-schema-session.js +4 -0
- package/dist/modes/rpc/method-classes-schema.d.ts +33 -0
- package/dist/modes/rpc/method-classes.d.ts +4 -0
- package/dist/modes/rpc/method-classes.js +7 -0
- package/dist/modes/rpc/multi-session-host.d.ts +2 -0
- package/dist/modes/rpc/multi-session-host.js +7 -2
- package/dist/modes/rpc/project-workspace-commands.d.ts +60 -0
- package/dist/modes/rpc/project-workspace-commands.js +145 -0
- package/dist/modes/rpc/rpc-error-types.d.ts +11 -1
- package/dist/modes/rpc/rpc-error-types.js +10 -0
- package/dist/modes/rpc/rpc-input-validation.js +4 -0
- package/dist/modes/rpc/rpc-mutation-response-types.d.ts +13 -0
- package/dist/modes/rpc/rpc-read-response-types.d.ts +13 -0
- package/dist/modes/rpc/rpc-session-command-types.d.ts +15 -0
- package/dist/modes/rpc/session-attachment-gate.js +2 -0
- package/dist/modes/rpc/session-command-activity.js +2 -0
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +41 -2
- package/docs/extensions.md +17 -1
- package/docs/rpc.md +121 -4
- package/docs/terminal-tools.md +8 -0
- package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
- package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-FUQJK2TU.js +0 -2
- package/dist/bundle/chunks/chunk-POZMRM6V.js +0 -2
- package/dist/bundle/chunks/chunk-XBG63MJZ.js +0 -8
- package/dist/bundle/chunks/chunk-YP47G4YP.js +0 -3
- package/dist/bundle/chunks/rpc-monitor-source-KKN2UP4T.js +0 -3
- package/dist/bundle/chunks/session-control-endpoint-OST7WAKP.js +0 -3
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const API_KEY_CREDENTIAL_FIELDS = {
|
|
2
|
+
type: "public",
|
|
3
|
+
key: "sealed",
|
|
4
|
+
env: "forces-device",
|
|
5
|
+
loginId: "device-only",
|
|
6
|
+
};
|
|
7
|
+
const OAUTH_CREDENTIAL_FIELDS = {
|
|
8
|
+
type: "public",
|
|
9
|
+
access: "sealed",
|
|
10
|
+
refresh: "device-only",
|
|
11
|
+
expires: "public",
|
|
12
|
+
idToken: "device-only",
|
|
13
|
+
subject: "device-only",
|
|
14
|
+
email: "device-only",
|
|
15
|
+
issuer: "device-only",
|
|
16
|
+
clientId: "device-only",
|
|
17
|
+
scopes: "device-only",
|
|
18
|
+
planUsageEnabled: "public",
|
|
19
|
+
// The ChatGPT sign-in route: a two-value protocol selector ("siwc" | "codex") the request needs, no secret material.
|
|
20
|
+
route: "public",
|
|
21
|
+
loginId: "device-only",
|
|
22
|
+
};
|
|
23
|
+
export const CREDENTIAL_SLOT_FIELDS = {
|
|
24
|
+
name: "public",
|
|
25
|
+
displayName: "device-only",
|
|
26
|
+
source: "public",
|
|
27
|
+
key: "sealed",
|
|
28
|
+
access: "sealed",
|
|
29
|
+
refresh: "device-only",
|
|
30
|
+
expires: "public",
|
|
31
|
+
// The slot's ChatGPT sign-in route: a two-value protocol selector ("siwc" | "codex"), no secret material.
|
|
32
|
+
route: "public",
|
|
33
|
+
env: "forces-device",
|
|
34
|
+
};
|
|
35
|
+
const POOLED_CREDENTIAL_FIELDS = {
|
|
36
|
+
accounts: CREDENTIAL_SLOT_FIELDS,
|
|
37
|
+
pinned: "public",
|
|
38
|
+
order: "public",
|
|
39
|
+
};
|
|
40
|
+
/** A stored credential key outside the declared fields (OAuth flows may add any) keeps the credential on the device. */
|
|
41
|
+
export function classifyCredentialField(type, key) {
|
|
42
|
+
const table = type === "api_key" ? API_KEY_CREDENTIAL_FIELDS : OAUTH_CREDENTIAL_FIELDS;
|
|
43
|
+
return (Object.hasOwn(table, key) ? table[key] : undefined) ?? "forces-device";
|
|
44
|
+
}
|
|
45
|
+
function forcesDevice(table, key, value) {
|
|
46
|
+
const fieldClass = Object.hasOwn(table, key) ? table[key] : undefined;
|
|
47
|
+
if (fieldClass === undefined)
|
|
48
|
+
return ["undeclared-field"];
|
|
49
|
+
if (fieldClass !== "forces-device")
|
|
50
|
+
return [];
|
|
51
|
+
return typeof value === "object" && value !== null && Object.keys(value).length === 0 ? [] : ["env"];
|
|
52
|
+
}
|
|
53
|
+
/** Why a stored auth.json credential must be used on the device; empty when sealed custody can compose its requests. */
|
|
54
|
+
export function credentialDeviceReasons(credential) {
|
|
55
|
+
const reasons = new Set();
|
|
56
|
+
const typeTable = credential.type === "api_key"
|
|
57
|
+
? API_KEY_CREDENTIAL_FIELDS
|
|
58
|
+
: credential.type === "oauth"
|
|
59
|
+
? OAUTH_CREDENTIAL_FIELDS
|
|
60
|
+
: undefined;
|
|
61
|
+
if (typeTable === undefined)
|
|
62
|
+
return ["undeclared-field"];
|
|
63
|
+
const table = { ...typeTable, ...POOLED_CREDENTIAL_FIELDS };
|
|
64
|
+
for (const [key, value] of Object.entries(credential)) {
|
|
65
|
+
for (const reason of forcesDevice(table, key, value))
|
|
66
|
+
reasons.add(reason);
|
|
67
|
+
}
|
|
68
|
+
const accounts = credential.accounts;
|
|
69
|
+
if (accounts !== undefined && !Array.isArray(accounts))
|
|
70
|
+
reasons.add("undeclared-field");
|
|
71
|
+
for (const account of Array.isArray(accounts) ? accounts : []) {
|
|
72
|
+
if (typeof account !== "object" || account === null) {
|
|
73
|
+
reasons.add("undeclared-field");
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const [key, value] of Object.entries(account)) {
|
|
77
|
+
for (const reason of forcesDevice(CREDENTIAL_SLOT_FIELDS, key, value))
|
|
78
|
+
reasons.add(reason);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [...reasons];
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=credential-fields.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The custody sidecar `<agentDir>/custody.json` (custody design 4.1, row CK5).
|
|
3
|
+
*
|
|
4
|
+
* It lives beside models.json, never inside it, so fused parsers and older CLIs never see a custody
|
|
5
|
+
* field. Only the device service writes it (temp file + rename); the fused CLI never reads it.
|
|
6
|
+
* A read is tri-state so a caller can tell "not enabled" (absent) from "broken" (invalid).
|
|
7
|
+
*/
|
|
8
|
+
import { type Static, Type } from "typebox";
|
|
9
|
+
declare const ProviderCustodySchema: Type.TObject<{
|
|
10
|
+
custody: Type.TOptional<Type.TUnion<[Type.TLiteral<"sealed">, Type.TLiteral<"device">]>>;
|
|
11
|
+
apiKeyTtlSeconds: Type.TOptional<Type.TInteger>;
|
|
12
|
+
headersTtlSeconds: Type.TOptional<Type.TInteger>;
|
|
13
|
+
sealedCwdHash: Type.TOptional<Type.TString>;
|
|
14
|
+
secretUrlParts: Type.TOptional<Type.TArray<Type.TString>>;
|
|
15
|
+
}>;
|
|
16
|
+
declare const CustodySidecarSchema: Type.TObject<{
|
|
17
|
+
v: Type.TLiteral<1>;
|
|
18
|
+
providers: Type.TRecord<"^.*$", Type.TObject<{
|
|
19
|
+
custody: Type.TOptional<Type.TUnion<[Type.TLiteral<"sealed">, Type.TLiteral<"device">]>>;
|
|
20
|
+
apiKeyTtlSeconds: Type.TOptional<Type.TInteger>;
|
|
21
|
+
headersTtlSeconds: Type.TOptional<Type.TInteger>;
|
|
22
|
+
sealedCwdHash: Type.TOptional<Type.TString>;
|
|
23
|
+
secretUrlParts: Type.TOptional<Type.TArray<Type.TString>>;
|
|
24
|
+
}>>;
|
|
25
|
+
consent: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
|
|
26
|
+
withdrawal: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
|
|
27
|
+
}>;
|
|
28
|
+
export type CustodySidecar = Static<typeof CustodySidecarSchema>;
|
|
29
|
+
export type ProviderCustody = Static<typeof ProviderCustodySchema>;
|
|
30
|
+
export type CustodySidecarRead = {
|
|
31
|
+
readonly state: "absent";
|
|
32
|
+
} | {
|
|
33
|
+
readonly state: "ok";
|
|
34
|
+
readonly sidecar: CustodySidecar;
|
|
35
|
+
} | {
|
|
36
|
+
readonly state: "invalid";
|
|
37
|
+
readonly reason: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function custodySidecarPath(agentDir: string): string;
|
|
40
|
+
export declare function readCustodySidecar(path: string): Promise<CustodySidecarRead>;
|
|
41
|
+
/** Writes the whole sidecar atomically: a 0600 temp file in the same directory, fsync, rename, fsync dir. */
|
|
42
|
+
export declare function writeCustodySidecar(path: string, sidecar: CustodySidecar): Promise<void>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=custody-sidecar.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The custody sidecar `<agentDir>/custody.json` (custody design 4.1, row CK5).
|
|
3
|
+
*
|
|
4
|
+
* It lives beside models.json, never inside it, so fused parsers and older CLIs never see a custody
|
|
5
|
+
* field. Only the device service writes it (temp file + rename); the fused CLI never reads it.
|
|
6
|
+
* A read is tri-state so a caller can tell "not enabled" (absent) from "broken" (invalid).
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { open, readFile, rename, rm } from "node:fs/promises";
|
|
10
|
+
import { basename, dirname, join } from "node:path";
|
|
11
|
+
import { Type } from "typebox";
|
|
12
|
+
import { Compile } from "typebox/compile";
|
|
13
|
+
const CUSTODY_SIDECAR_FILE = "custody.json";
|
|
14
|
+
const SignedRecordSchema = Type.Record(Type.String(), Type.Unknown());
|
|
15
|
+
const ProviderCustodySchema = Type.Object({
|
|
16
|
+
custody: Type.Optional(Type.Union([Type.Literal("sealed"), Type.Literal("device")])),
|
|
17
|
+
apiKeyTtlSeconds: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
18
|
+
headersTtlSeconds: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
19
|
+
sealedCwdHash: Type.Optional(Type.String({ minLength: 1 })),
|
|
20
|
+
secretUrlParts: Type.Optional(Type.Array(Type.String({ pattern: "^(urlPath:(0|[1-9][0-9]*)|urlQuery:.+)$" }), { uniqueItems: true })),
|
|
21
|
+
}, { additionalProperties: false });
|
|
22
|
+
const CustodySidecarSchema = Type.Object({
|
|
23
|
+
v: Type.Literal(1),
|
|
24
|
+
providers: Type.Record(Type.String({ minLength: 1 }), ProviderCustodySchema),
|
|
25
|
+
consent: Type.Optional(SignedRecordSchema),
|
|
26
|
+
withdrawal: Type.Optional(SignedRecordSchema),
|
|
27
|
+
}, { additionalProperties: false });
|
|
28
|
+
const validateCustodySidecar = Compile(CustodySidecarSchema);
|
|
29
|
+
export function custodySidecarPath(agentDir) {
|
|
30
|
+
return join(agentDir, CUSTODY_SIDECAR_FILE);
|
|
31
|
+
}
|
|
32
|
+
function parseCustodySidecar(text) {
|
|
33
|
+
let value;
|
|
34
|
+
try {
|
|
35
|
+
value = JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof SyntaxError)
|
|
39
|
+
return { state: "invalid", reason: `not JSON: ${error.message}` };
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
if (validateCustodySidecar.Check(value))
|
|
43
|
+
return { state: "ok", sidecar: value };
|
|
44
|
+
const first = validateCustodySidecar.Errors(value)[0];
|
|
45
|
+
return { state: "invalid", reason: first ? `${first.instancePath || "/"} ${first.message}` : "schema mismatch" };
|
|
46
|
+
}
|
|
47
|
+
export async function readCustodySidecar(path) {
|
|
48
|
+
let text;
|
|
49
|
+
try {
|
|
50
|
+
text = await readFile(path, "utf8");
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (isErrnoCode(error, "ENOENT"))
|
|
54
|
+
return { state: "absent" };
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
return parseCustodySidecar(text);
|
|
58
|
+
}
|
|
59
|
+
/** Writes the whole sidecar atomically: a 0600 temp file in the same directory, fsync, rename, fsync dir. */
|
|
60
|
+
export async function writeCustodySidecar(path, sidecar) {
|
|
61
|
+
if (!validateCustodySidecar.Check(sidecar))
|
|
62
|
+
throw new TypeError("custody sidecar does not match its schema");
|
|
63
|
+
const directory = dirname(path);
|
|
64
|
+
const temp = join(directory, `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
65
|
+
const file = await open(temp, "wx", 0o600);
|
|
66
|
+
try {
|
|
67
|
+
await file.writeFile(`${JSON.stringify(sidecar, null, "\t")}\n`, "utf8");
|
|
68
|
+
await file.sync();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await file.close();
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await rename(temp, path);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
await rm(temp, { force: true });
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
const dir = await open(directory, "r");
|
|
81
|
+
try {
|
|
82
|
+
await dir.sync();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await dir.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isErrnoCode(error, code) {
|
|
89
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=custody-sidecar.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a provider must stay on the device whatever the sidecar asks for (custody gate H1, M1).
|
|
3
|
+
*
|
|
4
|
+
* The models.json schema accepts unknown keys at every level, so the walk here applies
|
|
5
|
+
* `MODELS_JSON_FIELD_CLASSES` to the concrete provider object with whitelist semantics: a key the
|
|
6
|
+
* table does not name, at any depth, keeps the provider on the device instead of reaching the brain.
|
|
7
|
+
*/
|
|
8
|
+
import type { ModelsJsonProvider } from "../model-config-schema.ts";
|
|
9
|
+
export type DeviceCustodyReason = "undeclared-field" | "extra-body" | "sampling-params" | "unsplittable-base-url" | "model-base-url-secret" | "model-header-collision";
|
|
10
|
+
/** Empty when sealed custody may serve the provider. */
|
|
11
|
+
export declare function deviceCustodyReasons(provider: ModelsJsonProvider): readonly DeviceCustodyReason[];
|
|
12
|
+
//# sourceMappingURL=device-custody.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a provider must stay on the device whatever the sidecar asks for (custody gate H1, M1).
|
|
3
|
+
*
|
|
4
|
+
* The models.json schema accepts unknown keys at every level, so the walk here applies
|
|
5
|
+
* `MODELS_JSON_FIELD_CLASSES` to the concrete provider object with whitelist semantics: a key the
|
|
6
|
+
* table does not name, at any depth, keeps the provider on the device instead of reaching the brain.
|
|
7
|
+
*/
|
|
8
|
+
import { classifyModelsJsonField, MODELS_JSON_FIELD_CLASSES, MODELS_JSON_TYPE_ONLY_FIELDS, } from "./classify.js";
|
|
9
|
+
import { splitEndpoint } from "./endpoint.js";
|
|
10
|
+
const STOPS_WALK = new Set(["sealed", "device-only"]);
|
|
11
|
+
/** Every path in the tables plus each of its ancestors (`a.b[].c` -> `a`, `a.b`, `a.b[]`, `a.b[].c`). */
|
|
12
|
+
const KNOWN_PATHS = new Set([...Object.keys(MODELS_JSON_FIELD_CLASSES), ...Object.keys(MODELS_JSON_TYPE_ONLY_FIELDS)].flatMap((path) => [
|
|
13
|
+
...[...path.matchAll(/[.[]/g)].map((boundary) => path.slice(0, boundary.index)),
|
|
14
|
+
path,
|
|
15
|
+
]));
|
|
16
|
+
/** A property name that would read as path syntax (`a.b`, `m[]`, `*`, `{key}`) is never a declared field. */
|
|
17
|
+
const PATH_SYNTAX = /[.[\]*{}]/;
|
|
18
|
+
function isPlainObject(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function walk(value, path, reasons) {
|
|
22
|
+
const fieldClass = classifyModelsJsonField(path);
|
|
23
|
+
if (fieldClass === "forces-device") {
|
|
24
|
+
const sampling = path.endsWith("samplingParams.*");
|
|
25
|
+
if (!sampling)
|
|
26
|
+
reasons.add("extra-body");
|
|
27
|
+
else if (typeof value === "string" || (typeof value === "object" && value !== null))
|
|
28
|
+
reasons.add("sampling-params");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (fieldClass !== undefined && STOPS_WALK.has(fieldClass))
|
|
32
|
+
return;
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
for (const item of value)
|
|
35
|
+
walk(item, `${path}[]`, reasons);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!isPlainObject(value)) {
|
|
39
|
+
if (fieldClass === undefined)
|
|
40
|
+
reasons.add("undeclared-field");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const isRecord = classifyModelsJsonField(`${path}.{key}`) !== undefined;
|
|
44
|
+
for (const [key, child] of Object.entries(value)) {
|
|
45
|
+
const childPath = isRecord ? `${path}.*` : `${path}.${key}`;
|
|
46
|
+
if ((isRecord || !PATH_SYNTAX.test(key)) && KNOWN_PATHS.has(childPath))
|
|
47
|
+
walk(child, childPath, reasons);
|
|
48
|
+
else
|
|
49
|
+
reasons.add("undeclared-field");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function hasModelHeaderCollision(provider) {
|
|
53
|
+
return (provider.models ?? []).some((model) => {
|
|
54
|
+
const override = Object.hasOwn(provider.modelOverrides ?? {}, model.id)
|
|
55
|
+
? provider.modelOverrides?.[model.id]?.headers
|
|
56
|
+
: undefined;
|
|
57
|
+
if (override === undefined)
|
|
58
|
+
return false;
|
|
59
|
+
const overrideNames = new Set(Object.keys(override).map((name) => name.toLowerCase()));
|
|
60
|
+
return Object.keys(model.headers ?? {}).some((name) => overrideNames.has(name.toLowerCase()));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Empty when sealed custody may serve the provider. */
|
|
64
|
+
export function deviceCustodyReasons(provider) {
|
|
65
|
+
const reasons = new Set();
|
|
66
|
+
walk(provider, "providers.*", reasons);
|
|
67
|
+
if (provider.baseUrl !== undefined && !splitEndpoint(provider.baseUrl).ok)
|
|
68
|
+
reasons.add("unsplittable-base-url");
|
|
69
|
+
for (const model of provider.models ?? []) {
|
|
70
|
+
if (model.baseUrl === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
const split = splitEndpoint(model.baseUrl);
|
|
73
|
+
if (!split.ok)
|
|
74
|
+
reasons.add("unsplittable-base-url");
|
|
75
|
+
else if (split.secrets.size > 0)
|
|
76
|
+
reasons.add("model-base-url-secret");
|
|
77
|
+
}
|
|
78
|
+
if (hasModelHeaderCollision(provider))
|
|
79
|
+
reasons.add("model-header-collision");
|
|
80
|
+
return [...reasons];
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=device-custody.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Endpoint` split and the URL secret marking rule (custody design 4.1, row CK5).
|
|
3
|
+
*
|
|
4
|
+
* A provider `baseUrl` is split into its public parts, which travel as plain config, and its
|
|
5
|
+
* secret parts, which are sealed one field each. Secret iff: `userinfo` (always); a path segment
|
|
6
|
+
* or query value that is templated (`!command` or a `$VAR` reference, senpi's own template
|
|
7
|
+
* syntax); a part the user listed in `secretUrlParts`; or a query value whose NAME is key-ish.
|
|
8
|
+
* The key-ish list decides what is sealed, never the source digest.
|
|
9
|
+
*/
|
|
10
|
+
export type UrlSecretField = "urlUserinfo" | `urlPath:${number}` | `urlQuery:${string}`;
|
|
11
|
+
export type MarkableUrlPart = `urlPath:${number}` | `urlQuery:${string}`;
|
|
12
|
+
export interface SecretUrlPart {
|
|
13
|
+
readonly secret: true;
|
|
14
|
+
}
|
|
15
|
+
export interface Endpoint {
|
|
16
|
+
readonly scheme: "http" | "https";
|
|
17
|
+
readonly host: string;
|
|
18
|
+
readonly port?: number;
|
|
19
|
+
readonly path: readonly (string | SecretUrlPart)[];
|
|
20
|
+
readonly query?: Readonly<Record<string, string | SecretUrlPart>>;
|
|
21
|
+
readonly userinfo?: true;
|
|
22
|
+
}
|
|
23
|
+
export type EndpointSplit = {
|
|
24
|
+
readonly ok: true;
|
|
25
|
+
readonly endpoint: Endpoint;
|
|
26
|
+
readonly secrets: ReadonlyMap<UrlSecretField, string>;
|
|
27
|
+
} | {
|
|
28
|
+
readonly ok: false;
|
|
29
|
+
readonly reason: "unparseable" | "scheme" | "fragment" | "duplicate-query" | "valueless-query";
|
|
30
|
+
};
|
|
31
|
+
export declare function splitEndpoint(baseUrl: string, secretUrlParts?: readonly MarkableUrlPart[]): EndpointSplit;
|
|
32
|
+
export declare function composeEndpointUrl(endpoint: Endpoint, secret: (field: UrlSecretField) => string): string;
|
|
33
|
+
//# sourceMappingURL=endpoint.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Endpoint` split and the URL secret marking rule (custody design 4.1, row CK5).
|
|
3
|
+
*
|
|
4
|
+
* A provider `baseUrl` is split into its public parts, which travel as plain config, and its
|
|
5
|
+
* secret parts, which are sealed one field each. Secret iff: `userinfo` (always); a path segment
|
|
6
|
+
* or query value that is templated (`!command` or a `$VAR` reference, senpi's own template
|
|
7
|
+
* syntax); a part the user listed in `secretUrlParts`; or a query value whose NAME is key-ish.
|
|
8
|
+
* The key-ish list decides what is sealed, never the source digest.
|
|
9
|
+
*/
|
|
10
|
+
import { getConfigValueEnvVarNames, isCommandConfigValue } from "../resolve-config-value.js";
|
|
11
|
+
/** Query parameter names whose value is sealed whatever it looks like (compared case-insensitively). */
|
|
12
|
+
const KEYISH_QUERY_NAMES = [
|
|
13
|
+
"key",
|
|
14
|
+
"api_key",
|
|
15
|
+
"api-key",
|
|
16
|
+
"apikey",
|
|
17
|
+
"token",
|
|
18
|
+
"access_token",
|
|
19
|
+
"sig",
|
|
20
|
+
"signature",
|
|
21
|
+
"sv",
|
|
22
|
+
"client_secret",
|
|
23
|
+
"secret",
|
|
24
|
+
"password",
|
|
25
|
+
"auth",
|
|
26
|
+
"authorization",
|
|
27
|
+
"auth_token",
|
|
28
|
+
"access-token",
|
|
29
|
+
"subscription-key",
|
|
30
|
+
"x-api-key",
|
|
31
|
+
];
|
|
32
|
+
const SECRET = { secret: true };
|
|
33
|
+
const URL_SHAPE = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)(\/[^?#]*)?(?:\?([^#]*))?(#.*)?$/i;
|
|
34
|
+
const HOST_PORT = /^([^@/?#:[\]]+|\[[^\]]+\])(?::(\d+))?$/;
|
|
35
|
+
function isTemplatedUrlPart(raw) {
|
|
36
|
+
return isCommandConfigValue(raw) || getConfigValueEnvVarNames(raw).length > 0;
|
|
37
|
+
}
|
|
38
|
+
function isKeyishQueryName(name) {
|
|
39
|
+
return KEYISH_QUERY_NAMES.includes(name.toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
export function splitEndpoint(baseUrl, secretUrlParts = []) {
|
|
42
|
+
const match = URL_SHAPE.exec(baseUrl);
|
|
43
|
+
if (!match)
|
|
44
|
+
return { ok: false, reason: "unparseable" };
|
|
45
|
+
const [, rawScheme, authority = "", rawPath, rawQuery, fragment] = match;
|
|
46
|
+
const at = authority.lastIndexOf("@");
|
|
47
|
+
const userinfo = at === -1 ? undefined : authority.slice(0, at);
|
|
48
|
+
const hostPort = HOST_PORT.exec(at === -1 ? authority : authority.slice(at + 1));
|
|
49
|
+
const host = hostPort?.[1];
|
|
50
|
+
const port = hostPort?.[2];
|
|
51
|
+
if (rawScheme === undefined || host === undefined)
|
|
52
|
+
return { ok: false, reason: "unparseable" };
|
|
53
|
+
if (fragment !== undefined)
|
|
54
|
+
return { ok: false, reason: "fragment" };
|
|
55
|
+
const scheme = rawScheme.toLowerCase();
|
|
56
|
+
if (scheme !== "http" && scheme !== "https")
|
|
57
|
+
return { ok: false, reason: "scheme" };
|
|
58
|
+
const marked = new Set(secretUrlParts);
|
|
59
|
+
const secrets = new Map();
|
|
60
|
+
if (userinfo !== undefined)
|
|
61
|
+
secrets.set("urlUserinfo", userinfo);
|
|
62
|
+
const path = (rawPath ?? "").split("/").slice(1);
|
|
63
|
+
const publicPath = path.map((segment, index) => {
|
|
64
|
+
const field = `urlPath:${index}`;
|
|
65
|
+
if (!isTemplatedUrlPart(segment) && !marked.has(field))
|
|
66
|
+
return segment;
|
|
67
|
+
secrets.set(field, segment);
|
|
68
|
+
return SECRET;
|
|
69
|
+
});
|
|
70
|
+
let query;
|
|
71
|
+
if (rawQuery !== undefined) {
|
|
72
|
+
query = {};
|
|
73
|
+
for (const pair of rawQuery.length > 0 ? rawQuery.split("&") : []) {
|
|
74
|
+
const separator = pair.indexOf("=");
|
|
75
|
+
if (separator < 1)
|
|
76
|
+
return { ok: false, reason: "valueless-query" };
|
|
77
|
+
const name = pair.slice(0, separator);
|
|
78
|
+
const value = pair.slice(separator + 1);
|
|
79
|
+
if (Object.hasOwn(query, name))
|
|
80
|
+
return { ok: false, reason: "duplicate-query" };
|
|
81
|
+
const field = `urlQuery:${name}`;
|
|
82
|
+
if (isKeyishQueryName(name) || isTemplatedUrlPart(value) || marked.has(field)) {
|
|
83
|
+
secrets.set(field, value);
|
|
84
|
+
query[name] = SECRET;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
query[name] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const endpoint = {
|
|
92
|
+
scheme,
|
|
93
|
+
host,
|
|
94
|
+
...(port !== undefined ? { port: Number(port) } : {}),
|
|
95
|
+
path: publicPath,
|
|
96
|
+
...(query !== undefined ? { query } : {}),
|
|
97
|
+
...(userinfo !== undefined ? { userinfo: true } : {}),
|
|
98
|
+
};
|
|
99
|
+
return { ok: true, endpoint, secrets };
|
|
100
|
+
}
|
|
101
|
+
export function composeEndpointUrl(endpoint, secret) {
|
|
102
|
+
const userinfo = endpoint.userinfo ? `${secret("urlUserinfo")}@` : "";
|
|
103
|
+
const port = endpoint.port !== undefined ? `:${endpoint.port}` : "";
|
|
104
|
+
const path = endpoint.path
|
|
105
|
+
.map((segment, index) => (typeof segment === "string" ? segment : secret(`urlPath:${index}`)))
|
|
106
|
+
.map((segment) => `/${segment}`)
|
|
107
|
+
.join("");
|
|
108
|
+
const query = endpoint.query === undefined
|
|
109
|
+
? ""
|
|
110
|
+
: `?${Object.entries(endpoint.query)
|
|
111
|
+
.map(([name, value]) => `${name}=${typeof value === "string" ? value : secret(`urlQuery:${name}`)}`)
|
|
112
|
+
.join("&")}`;
|
|
113
|
+
return `${endpoint.scheme}://${userinfo}${endpoint.host}${port}${path}${query}`;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=endpoint.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Agent, type AgentToolResult, type AgentToolUpdateCallback, type PreparedAgentToolCall } from "@earendil-works/pi-agent-core";
|
|
2
|
+
type HostAgent = Pick<Agent, "batchStoreFactory" | "executionScope" | "getExecutionScope" | "onExecutionPipeline">;
|
|
3
|
+
export interface ExecuteToolHookResult {
|
|
4
|
+
readonly content?: AgentToolResult<unknown>["content"];
|
|
5
|
+
readonly details?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface ExecuteToolHostSession {
|
|
8
|
+
readonly agent: HostAgent;
|
|
9
|
+
afterHooks(prepared: PreparedAgentToolCall, result: AgentToolResult<unknown>, isError: boolean): Promise<ExecuteToolHookResult | undefined>;
|
|
10
|
+
}
|
|
11
|
+
export interface ExecuteToolHostOptions {
|
|
12
|
+
readonly signal?: AbortSignal;
|
|
13
|
+
readonly onUpdate?: AgentToolUpdateCallback<unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface ExecuteToolHostOutcome {
|
|
16
|
+
readonly result: AgentToolResult<unknown>;
|
|
17
|
+
readonly hook: ExecuteToolHookResult | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Runs a call `executeTool()` already prepared and preflighted on the in-process prepared ToolHost (S19a). The host
|
|
21
|
+
* freezes the preflighted arguments; the caller's own signal and `onUpdate` reach the body, since `executeTool()` never
|
|
22
|
+
* raced an abort.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runExecuteToolOnHost(session: ExecuteToolHostSession, prepared: PreparedAgentToolCall, options: ExecuteToolHostOptions): Promise<ExecuteToolHostOutcome>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=execute-tool-on-host.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { BatchStoreMissingError, createPreparedToolHost, } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { carryPreflightFacts } from "./execution/preflight-carry.js";
|
|
4
|
+
const PREFLIGHTED_ARGUMENTS = Type.Unknown();
|
|
5
|
+
function issuingMessage(prepared) {
|
|
6
|
+
return {
|
|
7
|
+
role: "assistant",
|
|
8
|
+
content: [prepared.toolCall],
|
|
9
|
+
api: "execute-tool",
|
|
10
|
+
provider: "senpi",
|
|
11
|
+
model: "execute-tool",
|
|
12
|
+
usage: {
|
|
13
|
+
input: 0,
|
|
14
|
+
output: 0,
|
|
15
|
+
cacheRead: 0,
|
|
16
|
+
cacheWrite: 0,
|
|
17
|
+
totalTokens: 0,
|
|
18
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
19
|
+
},
|
|
20
|
+
stopReason: "toolUse",
|
|
21
|
+
timestamp: Date.now(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function thrownResult(error) {
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
27
|
+
details: { isError: true },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Runs a call `executeTool()` already prepared and preflighted on the in-process prepared ToolHost (S19a). The host
|
|
32
|
+
* freezes the preflighted arguments; the caller's own signal and `onUpdate` reach the body, since `executeTool()` never
|
|
33
|
+
* raced an abort.
|
|
34
|
+
*/
|
|
35
|
+
export async function runExecuteToolOnHost(session, prepared, options) {
|
|
36
|
+
const openStore = session.agent.batchStoreFactory;
|
|
37
|
+
if (openStore === undefined)
|
|
38
|
+
throw new BatchStoreMissingError();
|
|
39
|
+
const tool = prepared.tool;
|
|
40
|
+
let failure;
|
|
41
|
+
let settled;
|
|
42
|
+
let threw = false;
|
|
43
|
+
const body = Object.create(tool, {
|
|
44
|
+
prepareArguments: { value: undefined },
|
|
45
|
+
// The `tool_call` chain may rewrite the arguments, and its contract is that they are not validated again.
|
|
46
|
+
parameters: { value: PREFLIGHTED_ARGUMENTS },
|
|
47
|
+
execute: {
|
|
48
|
+
value: async (toolCallId, args) => {
|
|
49
|
+
try {
|
|
50
|
+
return await tool.execute(toolCallId, args, options.signal, options.onUpdate);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
threw = true;
|
|
54
|
+
return thrownResult(error);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const host = createPreparedToolHost({
|
|
60
|
+
afterToolFreeze: carryPreflightFacts,
|
|
61
|
+
beforeToolCall: async ({ args }) => {
|
|
62
|
+
carryPreflightFacts(prepared.args, args);
|
|
63
|
+
return undefined;
|
|
64
|
+
},
|
|
65
|
+
afterToolCall: async ({ result, isError }) => {
|
|
66
|
+
try {
|
|
67
|
+
settled = { result, hook: await session.afterHooks(prepared, result, isError || threw) };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
failure = { error };
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
},
|
|
74
|
+
removedToolHints: {},
|
|
75
|
+
}, openStore);
|
|
76
|
+
const agent = session.agent;
|
|
77
|
+
await host.execute({
|
|
78
|
+
scope: agent.getExecutionScope?.() ?? agent.executionScope,
|
|
79
|
+
calls: [{ ...prepared.toolCall, arguments: prepared.args }],
|
|
80
|
+
mode: "sequential",
|
|
81
|
+
}, {
|
|
82
|
+
emit: () => undefined,
|
|
83
|
+
...(agent.onExecutionPipeline === undefined ? {} : { recordWitness: agent.onExecutionPipeline }),
|
|
84
|
+
local: {
|
|
85
|
+
context: { systemPrompt: "", messages: [], tools: [body] },
|
|
86
|
+
assistantMessage: issuingMessage(prepared),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
if (failure !== undefined)
|
|
90
|
+
throw failure.error;
|
|
91
|
+
if (settled === undefined) {
|
|
92
|
+
throw new Error(`executeTool: the prepared host settled no result for ${prepared.toolCall.name}`);
|
|
93
|
+
}
|
|
94
|
+
return settled;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=execute-tool-on-host.js.map
|
|
@@ -137,6 +137,15 @@ function hasSafeToolResultEnvelope(message) {
|
|
|
137
137
|
(message.addedToolNames === undefined ||
|
|
138
138
|
(Array.isArray(message.addedToolNames) && message.addedToolNames.every((name) => typeof name === "string"))));
|
|
139
139
|
}
|
|
140
|
+
/** Reasoning-effort values a `configuration_update` entry can carry: the producer (`appendConfigurationUpdate`) writes a ThinkingLevel. */
|
|
141
|
+
const CONFIGURATION_UPDATE_EFFORTS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
142
|
+
function hasSafeConfigurationUpdate(message) {
|
|
143
|
+
return (typeof message.effort === "string" &&
|
|
144
|
+
CONFIGURATION_UPDATE_EFFORTS.has(message.effort) &&
|
|
145
|
+
Array.isArray(message.content) &&
|
|
146
|
+
message.content.length === 0 &&
|
|
147
|
+
isFiniteNumber(message.timestamp));
|
|
148
|
+
}
|
|
140
149
|
/** Reject any retained message that cannot be safely replayed through normalized provider conversion. */
|
|
141
150
|
export function hasUnsafeRetainedContent(messages) {
|
|
142
151
|
for (const value of messages) {
|
|
@@ -155,6 +164,12 @@ export function hasUnsafeRetainedContent(messages) {
|
|
|
155
164
|
if (!hasSafeToolResultEnvelope(value))
|
|
156
165
|
return true;
|
|
157
166
|
break;
|
|
167
|
+
case "configurationUpdate":
|
|
168
|
+
// A reasoning-effort change is a valid runtime message: normal request
|
|
169
|
+
// conversion accepts it, so a bounded one is replay-safe (senpi#8566).
|
|
170
|
+
if (!hasSafeConfigurationUpdate(value))
|
|
171
|
+
return true;
|
|
172
|
+
break;
|
|
158
173
|
case "custom":
|
|
159
174
|
if (typeof value.customType !== "string" ||
|
|
160
175
|
typeof value.display !== "boolean" ||
|
|
@@ -16,7 +16,7 @@ import { GOAL_CONTINUATION_SCHEDULED_EVENT, MonitorAwareGoalContinuation } from
|
|
|
16
16
|
import { reengageGoalAfterReload } from "./reload-reengagement.js";
|
|
17
17
|
import { registerGoalFireNowRequest } from "./rpc-wait-surface.js";
|
|
18
18
|
import { isStaleExtensionContextError } from "./stale-context.js";
|
|
19
|
-
import { markStopPaused, markStopResumed, pausedByStop, stopHoldLive } from "./stop-pause.js";
|
|
19
|
+
import { interruptedRunHeld, markStopPaused, markStopResumed, pausedByStop, stopHoldLive } from "./stop-pause.js";
|
|
20
20
|
import { accountGoalUsage, migrateLegacyGoal, readGoal, updateGoal } from "./store.js";
|
|
21
21
|
import { GOAL_STORE_CHANGED_EVENT, isGoalStoreChangedEvent } from "./store-changed-event.js";
|
|
22
22
|
import { goalStoreRef as buildGoalStoreRef } from "./store-ref.js";
|
|
@@ -155,7 +155,19 @@ export default function goalExtension(pi) {
|
|
|
155
155
|
todoOwedBackstop.resetChain();
|
|
156
156
|
const ref = goalStoreRef(ctx);
|
|
157
157
|
await migrateLegacyGoal(ref);
|
|
158
|
-
|
|
158
|
+
let goal = await readGoal(goalStoreRef(ctx));
|
|
159
|
+
// A process that died mid-turn never ran the quit-time abort that stops an active goal (senpi#5753). Stop it the
|
|
160
|
+
// same way here, so the resume prompt below asks before any continuation runs (senpi#5755). Opening that session
|
|
161
|
+
// at launch (`--continue`, `--session`) is the same resume, so it asks too.
|
|
162
|
+
let stoppedInterruptedGoal = false;
|
|
163
|
+
if ((event.reason === "resume" || event.reason === "startup") &&
|
|
164
|
+
ctx.hasUI &&
|
|
165
|
+
goal?.status === "active" &&
|
|
166
|
+
goal.continuationStoppedAt === undefined &&
|
|
167
|
+
interruptedRunHeld(ctx)) {
|
|
168
|
+
goal = await pauseOrBlockOnUserAbort(ctx, goal.id);
|
|
169
|
+
stoppedInterruptedGoal = true;
|
|
170
|
+
}
|
|
159
171
|
if (goal?.status === "active") {
|
|
160
172
|
beginAgentGoalAccounting(goal);
|
|
161
173
|
}
|
|
@@ -172,7 +184,7 @@ export default function goalExtension(pi) {
|
|
|
172
184
|
ctx.ui.notify(GOAL_STALE_STOP_NOTICE, "info");
|
|
173
185
|
return;
|
|
174
186
|
}
|
|
175
|
-
if (await maybePromptResumeStoppedGoal(pi, ctx, event.reason, goal)) {
|
|
187
|
+
if (await maybePromptResumeStoppedGoal(pi, ctx, stoppedInterruptedGoal ? "resume" : event.reason, goal)) {
|
|
176
188
|
return;
|
|
177
189
|
}
|
|
178
190
|
if (goal && event.reason === "reload") {
|
|
@@ -2,6 +2,8 @@ import type { ExtensionAPI, ExtensionContext } from "../../types.ts";
|
|
|
2
2
|
export declare const GOAL_STOP_PAUSED_ENTRY = "goal-stop-paused";
|
|
3
3
|
export declare const GOAL_STOP_RESUMED_ENTRY = "goal-stop-resumed";
|
|
4
4
|
export declare function stopHoldLive(ctx: ExtensionContext): boolean;
|
|
5
|
+
/** The session reopened over a run that never finished: its process died mid-turn (senpi#4683, senpi#5755). */
|
|
6
|
+
export declare function interruptedRunHeld(ctx: ExtensionContext): boolean;
|
|
5
7
|
export declare function markStopPaused(pi: Pick<ExtensionAPI, "appendEntry">, goalId: string): void;
|
|
6
8
|
export declare function markStopResumed(pi: Pick<ExtensionAPI, "appendEntry">, goalId: string): void;
|
|
7
9
|
/** A goal the user's Stop paused (not `/goal pause`) resumes with the session (senpi#3811). */
|