@jini-ai/daemon 0.6.15-rc.2 → 0.6.16
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 +12 -0
- package/README.md +14 -4
- package/dist/agent-executor/codex/home-config.js +3 -2
- package/dist/agent-executor/codex/home-staging.js +2 -2
- package/dist/agent-executor/codex/isolation.js +4 -3
- package/dist/agent-executor/contracts.d.ts +1 -1
- package/dist/agent-executor/contracts.js +0 -1
- package/dist/agent-executor/event-translation.d.ts +3 -3
- package/dist/agent-executor/event-translation.js +6 -6
- package/dist/agent-executor/events.js +65 -51
- package/dist/agent-executor/index.d.ts +5 -5
- package/dist/agent-executor/index.js +5 -5
- package/dist/agent-executor/launch.js +30 -46
- package/dist/agent-executor/live-session-state.js +2 -2
- package/dist/agent-executor/mcp-bridge.js +5 -0
- package/dist/agent-executor/message-relay.js +5 -6
- package/dist/agent-executor/run-output.js +7 -2
- package/dist/ai-trace-lifecycle.d.ts +1 -1
- package/dist/ai-trace-lifecycle.js +1 -1
- package/dist/close-status.js +6 -2
- package/dist/core/omit-keys.d.ts +7 -0
- package/dist/core/omit-keys.js +12 -0
- package/dist/core/read-only-tools.js +5 -1
- package/dist/core/tool-audit.js +11 -7
- package/dist/core/tool-gaps.js +1 -1
- package/dist/delegated-result-correlation.js +2 -2
- package/dist/frontend-capability-tools.d.ts +2 -0
- package/dist/frontend-capability-tools.js +7 -1
- package/dist/frontend-session-registry.d.ts +42 -3
- package/dist/frontend-session-registry.js +61 -9
- package/dist/http/agents.d.ts +4 -0
- package/dist/http/attachments.js +13 -12
- package/dist/http/component-catalog.js +1 -1
- package/dist/http/connectors.js +7 -5
- package/dist/http/db-ops.js +5 -0
- package/dist/http/delegated-tools.js +5 -0
- package/dist/http/frontend-control.d.ts +13 -0
- package/dist/http/frontend-control.js +14 -3
- package/dist/http/frontend-guest-scope.d.ts +32 -0
- package/dist/http/frontend-guest-scope.js +63 -0
- package/dist/http/frontend-sessions.d.ts +29 -6
- package/dist/http/frontend-sessions.js +108 -28
- package/dist/http/host-tools.js +7 -5
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/memory.js +3 -1
- package/dist/http/model-proxy.js +13 -8
- package/dist/http/research-tavily.adapter.js +2 -1
- package/dist/http/routines.js +7 -5
- package/dist/http/terminals.js +5 -0
- package/dist/http/tool-catalog.js +1 -1
- package/dist/http/xai.js +10 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/legacy-data-migration.d.ts +0 -12
- package/dist/legacy-data-migration.js +21 -13
- package/dist/remote-tool-executor.js +2 -1
- package/dist/routines/schedule.js +2 -1
- package/dist/routines/scheduler.js +6 -7
- package/dist/run-lifecycle.js +2 -1
- package/dist/store/agent-sessions/sql.js +7 -3
- package/dist/store/event-log/sqlite-kernel.js +7 -2
- package/dist/store/event-log/sqlite.js +6 -2
- package/dist/surface-exchanges.js +3 -3
- package/dist/terminal-session.d.ts +51 -20
- package/dist/terminal-session.js +10 -94
- package/dist/terminal.adapter.d.ts +22 -0
- package/dist/terminal.adapter.js +47 -0
- package/dist/tool-executor.d.ts +4 -2
- package/dist/tool-executor.js +95 -15
- package/dist/tool-recovery/recovery.js +5 -3
- package/dist/tool-recovery/redaction.js +1 -2
- package/dist/tool-recovery/stack.js +5 -1
- package/dist/tool-result-media.d.ts +4 -0
- package/dist/tool-result-media.js +10 -3
- package/dist/tool-result-surfaces.js +10 -9
- package/package.json +30 -34
|
@@ -6,7 +6,7 @@ import { spawn as nodeSpawn, } from 'node:child_process';
|
|
|
6
6
|
import { promises as fsPromises, } from 'node:fs';
|
|
7
7
|
import { tmpdir, } from 'node:os';
|
|
8
8
|
import { join, } from 'node:path';
|
|
9
|
-
import { agentCapabilities, createClaudeFilesystemSupportProbe, claudeFilesystemRuntimePaths, claudeFilesystemSettings, validateFilesystemLaunch, validateReadOnlyExtraDirs, bindRuntimeAgentDef, defaultRuntimeState, getRuntimeModelDiscoveryDeps, userToolchainBinDirs, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
|
|
9
|
+
import { agentCapabilities, assessNativeToolPolicy, resolveNativeToolPolicy, createClaudeFilesystemSupportProbe, claudeFilesystemRuntimePaths, claudeFilesystemSettings, validateFilesystemLaunch, validateReadOnlyExtraDirs, bindRuntimeAgentDef, defaultRuntimeState, getRuntimeModelDiscoveryDeps, userToolchainBinDirs, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
|
|
10
10
|
import { createCommandInvocation } from '@jini-ai/platform/command';
|
|
11
11
|
import { collectProcessTreePids, listProcessSnapshots, stopProcesses, } from '@jini-ai/platform/process';
|
|
12
12
|
import { applyImagePromptDelivery, } from '../image-prompt-delivery.js';
|
|
@@ -580,8 +580,8 @@ export async function guardWindowsCommandLineBudget({ input, deps }) {
|
|
|
580
580
|
checkWindowsDirectExeCommandLineBudget({ def: input.def, resolvedBin: input.launchPath, args: input.args });
|
|
581
581
|
if (windowsBudgetError) {
|
|
582
582
|
await deps.releaseStagedResources();
|
|
583
|
-
// `failBeforeSpawn` always rejects (`Promise<never>`);
|
|
584
|
-
|
|
583
|
+
// `failBeforeSpawn` always rejects (`Promise<never>`); awaiting it propagates that rejection.
|
|
584
|
+
await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_PROMPT_TOO_LARGE', message: windowsBudgetError.message });
|
|
585
585
|
}
|
|
586
586
|
}
|
|
587
587
|
/**
|
|
@@ -631,29 +631,13 @@ export async function confirmChildSpawned({ input, deps }) {
|
|
|
631
631
|
}
|
|
632
632
|
catch (err) {
|
|
633
633
|
await deps.releaseStagedResources();
|
|
634
|
-
|
|
634
|
+
await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: failed to spawn agent "${input.def.id}": ${errorMessage(err)}` });
|
|
635
635
|
}
|
|
636
636
|
}
|
|
637
637
|
/** Only include declared capability restrictions; absence has a distinct downstream contract. */
|
|
638
638
|
function nativeCapabilityInput({ input }, _options = {}) {
|
|
639
639
|
return { ...(input.nativeCapabilities !== undefined ? { nativeCapabilities: input.nativeCapabilities } : {}) };
|
|
640
640
|
}
|
|
641
|
-
/** Switch names a person sees for each native capability (the host's settings use the same words). */
|
|
642
|
-
const NATIVE_CAPABILITY_LABELS = {
|
|
643
|
-
shell: 'Bash/shell', fileWrite: 'File edit/write', userMcp: 'Your MCP servers', subagents: 'Subagents/Task',
|
|
644
|
-
userSettings: 'Your skills and settings', web: 'Web fetch/search', workflows: 'Workflows', loginEnvironment: 'Your shell environment',
|
|
645
|
-
};
|
|
646
|
-
/** The refusal a person reads when a CLI without a native-tool policy cannot honor this run's switches:
|
|
647
|
-
* it names the CLI and the switch to turn on. Owner 2026-10-27: the old "no native-tool policy and
|
|
648
|
-
* audit adapter" told a Gemini user nothing. Pure, O(c) for c OFF switches.
|
|
649
|
-
* @example nativeToolPolicyRefusal({ cliName: 'Antigravity', offSwitches: ['userMcp'] }, {});
|
|
650
|
-
*/
|
|
651
|
-
function nativeToolPolicyRefusal({ cliName, offSwitches }, _options = {}) {
|
|
652
|
-
const switches = offSwitches.map(key => `'${NATIVE_CAPABILITY_LABELS[key] ?? key}'`);
|
|
653
|
-
const blocked = switches.length > 0 ? `with ${new Intl.ListFormat('en', { type: 'conjunction' }).format(switches)} off` : `with this chat's CLI tool limits`;
|
|
654
|
-
const action = switches.length === 0 ? `Turn on 'Full CLI access'` : `Turn ${switches.length === 1 ? 'it' : 'them'} on`;
|
|
655
|
-
return `${cliName} can't run ${blocked}. ${action} in the assistant settings, or choose Claude or Codex.`;
|
|
656
|
-
}
|
|
657
641
|
/** Correlation projects retained stdout, so exact-byte journals need a separate blob writer. */
|
|
658
642
|
function validateStdoutJournalOptions(options, _options = {}) {
|
|
659
643
|
if (options.delegatedResultCorrelation && options.journal && !options.stdoutJournalBlobs) {
|
|
@@ -709,13 +693,8 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
709
693
|
const { createCommandInvocation: createCommandInvocationFn, spawn: spawnFn, listProcessSnapshots: listProcessSnapshotsFn, collectProcessTreePids: collectProcessTreePidsFn, stopProcesses: stopProcessesFn, } = resolveProcessDeps(options);
|
|
710
694
|
const { onCleanupFailure: onCleanupFailureFn, bufferedStdoutMaxBytes } = resolveMiscExecutorDeps(options);
|
|
711
695
|
validateStdoutJournalOptions(options);
|
|
712
|
-
const journal = options
|
|
713
|
-
const continuation = options.continuation;
|
|
714
|
-
const classifyFailure = options.classifyFailure;
|
|
715
|
-
const mcpJsonInjection = options.mcpJsonInjection;
|
|
716
|
-
const claudeConfigDirIsolation = options.claudeConfigDirIsolation;
|
|
696
|
+
const { journal, continuation, classifyFailure, mcpJsonInjection, claudeConfigDirIsolation, promptAugmenter } = options;
|
|
717
697
|
const claudeConfigDirIsolationEnabled = options.claudeConfigDirIsolationEnabled ?? false;
|
|
718
|
-
const promptAugmenter = options.promptAugmenter;
|
|
719
698
|
const filesystemTemps = new Map();
|
|
720
699
|
const verifyClaudeFilesystem = options.assertClaudeFilesystemSupport ?? createClaudeFilesystemSupportProbe({});
|
|
721
700
|
/** Remove this run's private temp directories on startup failure, exit, crash or cancellation.
|
|
@@ -765,7 +744,9 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
765
744
|
/** `AgentExecutor.sendUserMessage()` — see that interface method's own doc. @complexity O(1). */
|
|
766
745
|
async function sendUserMessage({ runId, text, id = randomUUID(), attachmentIds }, { imageContents } = {}) {
|
|
767
746
|
const deliver = liveRunInputs.get(runId);
|
|
768
|
-
|
|
747
|
+
if (!deliver)
|
|
748
|
+
return 'not-running';
|
|
749
|
+
return deliver({ text, id, ...(attachmentIds ? { attachmentIds } : {}) }, imageContents ? { imageContents } : {});
|
|
769
750
|
}
|
|
770
751
|
/** Validates launch input and selects the definition synchronously before yielding. */
|
|
771
752
|
async function prepareRunContext({ requiredArgs, optionalArgs }, _options = {}) {
|
|
@@ -778,14 +759,13 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
778
759
|
return { input, ...resolved };
|
|
779
760
|
}
|
|
780
761
|
async function enforceNativeToolPolicy({ input, def }, _options = {}) {
|
|
762
|
+
Object.assign(input, resolveNativeToolPolicy({ def, policy: input }));
|
|
781
763
|
// Never silently ignore a host's locked native-tool policy on another CLI.
|
|
782
764
|
// Parity cannot override an explicit lock or a restricted run.
|
|
783
765
|
input.cliParity = input.cliParity === true && input.permissionMode === 'bypass' && input.nativeToolAccess !== false;
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (input.nativeToolAccess !== undefined && !input.cliParity || offSwitches.length > 0) {
|
|
788
|
-
return failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED', message: nativeToolPolicyRefusal({ cliName: def.name, offSwitches }, {}) });
|
|
766
|
+
const refusal = assessNativeToolPolicy({ def, policy: input });
|
|
767
|
+
if (refusal !== null) {
|
|
768
|
+
await failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED', message: refusal });
|
|
789
769
|
}
|
|
790
770
|
}
|
|
791
771
|
/** Validate every directory after image augmentation, before model probes or resource staging.
|
|
@@ -804,7 +784,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
804
784
|
input.cwd = input.filesystemRoot;
|
|
805
785
|
}
|
|
806
786
|
catch {
|
|
807
|
-
|
|
787
|
+
await failBeforeSpawn({ runId: input.runId, code: 'FILESYSTEM_POLICY_INVALID', message: 'Folder-limited launch refused: invalid filesystem root, working directory or attachment path.' });
|
|
808
788
|
}
|
|
809
789
|
}
|
|
810
790
|
/** Apply supported confinement without removing providers; unknown versions keep the settings.
|
|
@@ -829,12 +809,14 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
829
809
|
spawnEnv.TMP = input.filesystemTempDir;
|
|
830
810
|
spawnEnv.TEMP = input.filesystemTempDir;
|
|
831
811
|
if (def.id === 'claude' && process.platform !== 'win32') {
|
|
832
|
-
//
|
|
833
|
-
// plus /claude-<uid>
|
|
834
|
-
|
|
812
|
+
// Read denies also block create/unlink, so Claude's cwd files cannot live under /tmp.
|
|
813
|
+
// /var/tmp exists on macOS/Linux; its realpath plus /claude-<uid> must fit 44 bytes
|
|
814
|
+
// or the policy refuses launch instead of letting Bash fall back to shared scratch.
|
|
815
|
+
// mkdtemp creates a private 0700 directory; register cleanup before resolving aliases.
|
|
816
|
+
const runtimeTemp = await fsPromises.mkdtemp('/var/tmp/jini-');
|
|
835
817
|
filesystemTemps.get(input.runId).push(runtimeTemp);
|
|
836
|
-
input.filesystemRuntimeTempBase = runtimeTemp;
|
|
837
|
-
spawnEnv.CLAUDE_CODE_TMPDIR =
|
|
818
|
+
input.filesystemRuntimeTempBase = await fsPromises.realpath(runtimeTemp);
|
|
819
|
+
spawnEnv.CLAUDE_CODE_TMPDIR = input.filesystemRuntimeTempBase;
|
|
838
820
|
}
|
|
839
821
|
if (supported === false) {
|
|
840
822
|
input.filesystemNotice = 'files and shell are not folder-limited for this agent';
|
|
@@ -854,7 +836,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
854
836
|
spawnEnv.CLAUDE_CODE_DISABLE_AUTO_MEMORY = '1';
|
|
855
837
|
}
|
|
856
838
|
catch {
|
|
857
|
-
|
|
839
|
+
await failBeforeSpawn({ runId: input.runId, code: 'FILESYSTEM_POLICY_UNSUPPORTED', message: 'Folder-limited launch refused: invalid host settings or private runtime directory.' });
|
|
858
840
|
}
|
|
859
841
|
}
|
|
860
842
|
function resolveRunOverlay({ input, def, imageDelivery }, _options = {}) {
|
|
@@ -1055,10 +1037,9 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
1055
1037
|
}
|
|
1056
1038
|
/**
|
|
1057
1039
|
* `AgentExecutor.run()` — see that interface method's own doc for the
|
|
1058
|
-
* public contract. Implementation note on shape: every
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
* so each failure path reads as a single, obviously-terminal statement.
|
|
1040
|
+
* public contract. Implementation note on shape: every failure path below
|
|
1041
|
+
* awaits `failBeforeSpawn(...)` (a `Promise<never>`, so the await always
|
|
1042
|
+
* throws) and then returns, so each one reads as obviously terminal.
|
|
1062
1043
|
* @param input - `{runId, agentId, prompt, cwd, model?, reasoning?, imagePaths?, extraAllowedDirs?, uploadRoot?, env?}` — `runId` must already be `lifecycle.start()`-ed.
|
|
1063
1044
|
* @throws {@link AgentExecutorError} — see index.ts module doc's Invariant; never a bare `Error`.
|
|
1064
1045
|
* @complexity O(1) setup (registry lookup, launch resolution, one spawn call); steady-state cost thereafter belongs to {@link wireChildLifecycle}.
|
|
@@ -1255,7 +1236,8 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
1255
1236
|
const spawnResult = spawnAgentChildProcess({ input: { cwd: input.cwd, childEnv, invocation }, deps: { spawn: spawnFn } });
|
|
1256
1237
|
if (spawnResult.kind === 'error') {
|
|
1257
1238
|
await releaseStagedResources();
|
|
1258
|
-
|
|
1239
|
+
await failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: spawn threw synchronously for agent "${def.id}": ${errorMessage(spawnResult.error)}` });
|
|
1240
|
+
return;
|
|
1259
1241
|
}
|
|
1260
1242
|
const child = spawnResult.child;
|
|
1261
1243
|
// Registered before the spawn-confirmation await below, for the same reason
|
|
@@ -1282,10 +1264,12 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
1282
1264
|
// worse than releasing early (see `RuntimeLockHold.waitForHandoff`'s own doc).
|
|
1283
1265
|
armHandoffWatcher({ runtimeLockHold: runtimeLockHold, handoffInput: { logFilePath: preparedLogFile?.path, model: selectedModel, processExited: processExitedController.signal }, release: releaseRuntimeLock });
|
|
1284
1266
|
if (streamFormat === 'acp-json-rpc') {
|
|
1285
|
-
|
|
1267
|
+
await dispatchAcpPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, mcpBridge, overlayDelivery }, {});
|
|
1268
|
+
return;
|
|
1286
1269
|
}
|
|
1287
1270
|
if (streamFormat === 'pi-rpc') {
|
|
1288
|
-
|
|
1271
|
+
await dispatchPiPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, overlayDelivery }, {});
|
|
1272
|
+
return;
|
|
1289
1273
|
}
|
|
1290
1274
|
dispatchStdinPrompt({ input, def, child, overlayDelivery, imageDelivery, stdinHandle }, {});
|
|
1291
1275
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRecord } from './values.js';
|
|
2
|
+
import { omitKeys } from '../core/omit-keys.js';
|
|
2
3
|
/** Parse only records; malformed stdout remains the stream parser's responsibility. */
|
|
3
4
|
function frameRecord(line) {
|
|
4
5
|
try {
|
|
@@ -39,8 +40,7 @@ export function createLiveSessionState({ emit }, _options = {}) {
|
|
|
39
40
|
let backgroundTasks = 0;
|
|
40
41
|
function publishTask(task) {
|
|
41
42
|
tasks.set(task.id, task);
|
|
42
|
-
|
|
43
|
-
emit(payload);
|
|
43
|
+
emit(omitKeys(task, ['local']));
|
|
44
44
|
}
|
|
45
45
|
function taskDetails(frame, previous) {
|
|
46
46
|
const patch = isRecord(frame.patch) ? frame.patch : frame;
|
|
@@ -276,6 +276,11 @@ export function buildMcpBridgeDelivery(input) {
|
|
|
276
276
|
return { kind: 'codex-toml', serverEntry };
|
|
277
277
|
case 'env-passthrough':
|
|
278
278
|
return { kind: 'env-passthrough', serverEntry };
|
|
279
|
+
default: {
|
|
280
|
+
// Unreachable: every bridge strategy has a case above, and a new one fails typecheck here.
|
|
281
|
+
const unknownStrategy = strategy;
|
|
282
|
+
throw new TypeError(`Unknown MCP bridge strategy: ${String(unknownStrategy)}`);
|
|
283
|
+
}
|
|
279
284
|
}
|
|
280
285
|
}
|
|
281
286
|
function bridgeCredentialOptions(input) {
|
|
@@ -26,7 +26,8 @@ export function createMessageRelay({ emit }, { maxChars = 32_000 } = {}) {
|
|
|
26
26
|
enqueue({ id, text, hasAttachments }) {
|
|
27
27
|
if (closed)
|
|
28
28
|
return 'not-running';
|
|
29
|
-
|
|
29
|
+
seq += 1;
|
|
30
|
+
pending.push({ id, text, hasAttachments, seq });
|
|
30
31
|
emit({ type: 'user_message', id, text, deliveryState: 'queued' });
|
|
31
32
|
return 'delivered';
|
|
32
33
|
},
|
|
@@ -36,11 +37,9 @@ export function createMessageRelay({ emit }, { maxChars = 32_000 } = {}) {
|
|
|
36
37
|
let count = 0;
|
|
37
38
|
let chars = 0;
|
|
38
39
|
for (const message of pending) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Whole messages only; a single oversized message is delivered alone.
|
|
43
|
-
if (count > 0 && chars + size > maxChars)
|
|
40
|
+
// Whole messages only, stopping at an attachment; a single oversized message is delivered alone.
|
|
41
|
+
const size = message.hasAttachments ? 0 : JSON.stringify({ id: message.id, seq: message.seq, text: message.text }).length;
|
|
42
|
+
if (message.hasAttachments || (count > 0 && chars + size > maxChars))
|
|
44
43
|
break;
|
|
45
44
|
chars += size;
|
|
46
45
|
count++;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { redactSecrets } from '@jini-ai/core';
|
|
2
2
|
// Only a terminal-prefixed failure overrides real output; ordinary assistant explanations
|
|
3
3
|
// can mention empty output or headless permissions without being failed runs themselves.
|
|
4
|
-
|
|
4
|
+
// One pattern per diagnostic; a line is a diagnostic when any of them matches.
|
|
5
|
+
const DIAGNOSTIC_LINES = [
|
|
6
|
+
/^(?:jetski|agy|error|fatal):\s*no (?:output|response) produced(?:\s*[—–:-]|[.!]\s*$)/i,
|
|
7
|
+
/^(?:jetski|agy|error|fatal):\s*(?:error:\s*)?permission denied\b.*headless/i,
|
|
8
|
+
/^(?:jetski|agy|error|fatal):\s*headless\b.*auto-denied/i,
|
|
9
|
+
];
|
|
5
10
|
/** Records whether the event is assistant text, tool activity or a run error. */
|
|
6
11
|
function noteActivity(seen, event) {
|
|
7
12
|
if (event.kind === 'error')
|
|
@@ -23,7 +28,7 @@ function noteDiagnostic({ seen, tails, event, plainStdout }) {
|
|
|
23
28
|
return;
|
|
24
29
|
const channel = event.kind === 'stderr' ? 'stderr' : 'stdout';
|
|
25
30
|
tails[channel] = (tails[channel] + event.payload.chunk).slice(-8192);
|
|
26
|
-
const line = tails[channel].split(/\r?\n/).find(value =>
|
|
31
|
+
const line = tails[channel].split(/\r?\n/).find(value => DIAGNOSTIC_LINES.some(pattern => pattern.test(value.trim())));
|
|
27
32
|
if (line)
|
|
28
33
|
seen.diagnostic = line;
|
|
29
34
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module ai-trace-lifecycle
|
|
3
3
|
*
|
|
4
4
|
* Wraps a {@link RunLifecycle} so every run it drives is also recorded as an AI trace. This is
|
|
5
|
-
* the single tap for every provider path: child stream parsers (Claude Code,
|
|
5
|
+
* the single tap for every provider path: child stream parsers (Claude Code, Qoder,
|
|
6
6
|
* Codex/OpenCode/Cursor/Kimi JSON streams), ACP sessions and pi-rpc all publish their normalized
|
|
7
7
|
* events through `lifecycle.emit({ event: 'agent' })`. Observing the committed stream also sees
|
|
8
8
|
* watchdog-driven terminal transitions, which call the base lifecycle's closed-over `finish()`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module ai-trace-lifecycle
|
|
3
3
|
*
|
|
4
4
|
* Wraps a {@link RunLifecycle} so every run it drives is also recorded as an AI trace. This is
|
|
5
|
-
* the single tap for every provider path: child stream parsers (Claude Code,
|
|
5
|
+
* the single tap for every provider path: child stream parsers (Claude Code, Qoder,
|
|
6
6
|
* Codex/OpenCode/Cursor/Kimi JSON streams), ACP sessions and pi-rpc all publish their normalized
|
|
7
7
|
* events through `lifecycle.emit({ event: 'agent' })`. Observing the committed stream also sees
|
|
8
8
|
* watchdog-driven terminal transitions, which call the base lifecycle's closed-over `finish()`.
|
package/dist/close-status.js
CHANGED
|
@@ -30,6 +30,11 @@ export function classifyRunCloseStatus(requiredArgs, optionalArgs = {}) {
|
|
|
30
30
|
}
|
|
31
31
|
return 'failed';
|
|
32
32
|
}
|
|
33
|
+
/** A set environment value parsed as a positive, finite millisecond count, else undefined. */
|
|
34
|
+
function positiveTimeout(raw) {
|
|
35
|
+
const value = raw !== undefined ? Number(raw) : undefined;
|
|
36
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
37
|
+
}
|
|
33
38
|
/**
|
|
34
39
|
* Resolves a timeout in milliseconds from an env-var override, an
|
|
35
40
|
* agent-supplied default, and a kernel default, in that precedence order,
|
|
@@ -48,8 +53,7 @@ export function resolveTimeoutMs(requiredArgs, optionalArgs = {}) {
|
|
|
48
53
|
const input = { ...requiredArgs, ...optionalArgs };
|
|
49
54
|
const env = input.env ?? (typeof process !== 'undefined' ? process.env : {});
|
|
50
55
|
const rawFromEnv = input.envVar ? env[input.envVar] : undefined;
|
|
51
|
-
const
|
|
52
|
-
const candidate = fromEnv !== undefined && Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : (input.agentDefaultMs ?? input.defaultMs);
|
|
56
|
+
const candidate = positiveTimeout(rawFromEnv) ?? input.agentDefaultMs ?? input.defaultMs;
|
|
53
57
|
return Math.min(candidate, input.maxMs);
|
|
54
58
|
}
|
|
55
59
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Copy an object without some of its keys — what `const { a: _a, ...rest } = source` does,
|
|
3
|
+
* without binding a throwaway name for every dropped key.
|
|
4
|
+
*/
|
|
5
|
+
/** A shallow copy of `source`'s own enumerable properties, minus `keys`. @complexity O(own keys). */
|
|
6
|
+
export declare function omitKeys<T extends object, K extends keyof T>(source: T, keys: readonly K[]): Omit<T, K>;
|
|
7
|
+
//# sourceMappingURL=omit-keys.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Copy an object without some of its keys — what `const { a: _a, ...rest } = source` does,
|
|
3
|
+
* without binding a throwaway name for every dropped key.
|
|
4
|
+
*/
|
|
5
|
+
/** A shallow copy of `source`'s own enumerable properties, minus `keys`. @complexity O(own keys). */
|
|
6
|
+
export function omitKeys(source, keys) {
|
|
7
|
+
const copy = { ...source };
|
|
8
|
+
for (const key of keys)
|
|
9
|
+
delete copy[key];
|
|
10
|
+
return copy;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=omit-keys.js.map
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { isReadOnlyTool, toolDiscoveryMetadata } from '@jini-ai/core';
|
|
2
2
|
import { toolCandidates } from './tool-candidates.js';
|
|
3
|
+
/** ` — did you mean a, b?` for any close tool ids, else nothing. */
|
|
4
|
+
function didYouMean(candidates) {
|
|
5
|
+
return candidates.length > 0 ? ` — did you mean ${candidates.join(', ')}?` : '';
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Default daemon refusal wording, replaceable by the host. Unknown registrations and writes both
|
|
5
9
|
* refuse unsafe dispatch; unknown ids direct discovery because an unrestricted retry cannot run them.
|
|
@@ -11,7 +15,7 @@ export const defaultDaemonMessages = {
|
|
|
11
15
|
toolRefusalMessage: ({ toolId }) => `tool "${toolId}" is not registered as read-only — this gateway executes only tools whose registration declares readOnly; call it through execute_delegated_tool instead`,
|
|
12
16
|
unknownToolMessage: ({ toolId, candidates }) => toolId === 'check_messages'
|
|
13
17
|
? 'check_messages is an MCP-local tool; call check_messages directly, never as toolId in a delegated gateway'
|
|
14
|
-
: `no tool "${toolId}" is registered; use search_tools${candidates
|
|
18
|
+
: `no tool "${toolId}" is registered; use search_tools${didYouMean(candidates)}`,
|
|
15
19
|
},
|
|
16
20
|
};
|
|
17
21
|
/** @returns A copied, attenuated principal; its roles are unchanged. */
|
package/dist/core/tool-audit.js
CHANGED
|
@@ -49,17 +49,21 @@ function thrownAttempt({ error }, _optional = {}) {
|
|
|
49
49
|
return { phase: /unknown tool/i.test(error.message) ? 'unknown-tool' : 'failed', detail: error.name };
|
|
50
50
|
}
|
|
51
51
|
/** Settled details retain truncation or an errorId link, never error text or operator content. */
|
|
52
|
-
function settledDetail({ result, readErrorId }, options) {
|
|
53
|
-
|
|
54
|
-
return 'output truncated';
|
|
52
|
+
function settledDetail({ result, readErrorId, readAuditRecord }, options) {
|
|
53
|
+
let detail = result.truncated ? 'output truncated' : null;
|
|
55
54
|
// Host error-ID metadata is optional observation, never part of execution success.
|
|
56
55
|
try {
|
|
57
|
-
const id = readErrorId({ result });
|
|
58
|
-
|
|
56
|
+
const id = result.truncated ? undefined : readErrorId({ result });
|
|
57
|
+
if (id !== undefined)
|
|
58
|
+
detail = `errorId=${id}`;
|
|
59
|
+
const decision = readAuditRecord()?.events.find(event => event.detail?.startsWith('frontend approval: '))?.detail;
|
|
60
|
+
// A frontend may supply inputs, but only these fixed outcomes belong in the durable trail.
|
|
61
|
+
const approval = ['approved', 'declined', 'expired', 'abandoned'].map(value => `frontend approval: ${value}`).find(value => value === decision);
|
|
62
|
+
return [detail, approval].filter(Boolean).join('; ') || null;
|
|
59
63
|
}
|
|
60
64
|
catch (error) {
|
|
61
65
|
reportSinkError(error, options);
|
|
62
|
-
return
|
|
66
|
+
return detail;
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
/** Append requested BEFORE delegation: unknown tools and throwing authorization have no execution
|
|
@@ -83,7 +87,7 @@ export function withToolAttemptAudit(deps, options = {}) {
|
|
|
83
87
|
await appendSafely({ ...base, executionId: null, phase, at: deps.now(), detail }, deps, options);
|
|
84
88
|
throw error;
|
|
85
89
|
}
|
|
86
|
-
const detail = settledDetail({ result, readErrorId: deps.readErrorId }, options);
|
|
90
|
+
const detail = settledDetail({ result, readErrorId: deps.readErrorId, readAuditRecord: () => deps.inner.getAuditRecord({ executionId: result.executionId }) }, options);
|
|
87
91
|
await appendSafely({ ...base, executionId: result.executionId, phase: result.status, at: deps.now(), detail }, deps, options);
|
|
88
92
|
return result;
|
|
89
93
|
},
|
package/dist/core/tool-gaps.js
CHANGED
|
@@ -189,7 +189,7 @@ export function createToolGapCollector(required, optional = {}) {
|
|
|
189
189
|
const payload = event.payload;
|
|
190
190
|
if (payload.type === 'text_delta')
|
|
191
191
|
readDelta(payload.delta);
|
|
192
|
-
|
|
192
|
+
if (payload.type === 'tool_use')
|
|
193
193
|
recordToolUse(payload, event.ts);
|
|
194
194
|
},
|
|
195
195
|
/** Await outstanding audit writes during graceful shutdown/tests. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { omitKeys } from './core/omit-keys.js';
|
|
1
2
|
/** Accept only the complete base64 source shape; extra fields must remain part of the digest. */
|
|
2
3
|
function base64ImageSource({ source }, _options = {}) {
|
|
3
4
|
if (typeof source !== 'object' || source === null)
|
|
@@ -14,8 +15,7 @@ function normalizeSourceImage({ record }, _options = {}) {
|
|
|
14
15
|
const image = base64ImageSource({ source: record.source });
|
|
15
16
|
if (!image)
|
|
16
17
|
return record;
|
|
17
|
-
|
|
18
|
-
return { ...rest, data: image.data, mimeType: image.media_type };
|
|
18
|
+
return { ...omitKeys(record, ['source']), data: image.data, mimeType: image.media_type };
|
|
19
19
|
}
|
|
20
20
|
/** Normalize unambiguous CLI images and JSON object key order.
|
|
21
21
|
* @complexity O(n + k log k) for n bytes and k keys per object; inputs are wire JSON. */
|
|
@@ -58,6 +58,8 @@ export interface FrontendCapabilitySpec {
|
|
|
58
58
|
readonly description: string;
|
|
59
59
|
/** Surfaced on the descriptor so `ToolExecutor`'s confirmation gate enforces it. */
|
|
60
60
|
readonly requiresConfirmation?: boolean;
|
|
61
|
+
/** Protected page calls require a browser that supplies the human dialog channel. */
|
|
62
|
+
readonly requiresFrontendApproval?: boolean;
|
|
61
63
|
/** Carried onto `ToolDescriptor.inputSchema` so a discovery surface can state the arguments. */
|
|
62
64
|
readonly inputSchema?: unknown;
|
|
63
65
|
}
|
|
@@ -63,7 +63,13 @@ export function createFrontendCapabilityRegistrations(requiredArgs, optionalArgs
|
|
|
63
63
|
policy,
|
|
64
64
|
// `ctx.signal` is `ToolExecutor`'s own timeout/cancel signal, so the descriptor's `timeoutMs`
|
|
65
65
|
// and a cancelled run both reach the pending invocation without this module owning a timer.
|
|
66
|
-
handler: async (ctx
|
|
66
|
+
handler: async (ctx, options) => {
|
|
67
|
+
// page.approval advertises the bound tab's human dialog, not an executable tool.
|
|
68
|
+
if (capability.requiresFrontendApproval && registry.sessionFor({ runId: ctx.run.id }) && !registry.capabilitiesFor({ runId: ctx.run.id }).includes('page.approval')) {
|
|
69
|
+
throw new Error('Attached frontend has no page approval dialog; reload the page.');
|
|
70
|
+
}
|
|
71
|
+
return registry.invoke({ runId: ctx.run.id, capabilityId: capability.id, input: toCapabilityInput(capability.id, ctx.input) }, { signal: ctx.signal, ...(options?.onApprovalStateChange ? { onApprovalStateChange: options.onApprovalStateChange } : {}) });
|
|
72
|
+
},
|
|
67
73
|
}));
|
|
68
74
|
}
|
|
69
75
|
//# sourceMappingURL=frontend-capability-tools.js.map
|
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
import type { ToolApprovalDecision, ToolExecutionOptions } from '@jini-ai/core';
|
|
2
|
+
/** Host-authenticated ownership, never derived from a model/run context blob. */
|
|
3
|
+
export interface FrontendSessionOwner {
|
|
4
|
+
readonly workspaceId: string;
|
|
5
|
+
readonly principalId: string;
|
|
6
|
+
readonly tabId: string;
|
|
7
|
+
}
|
|
8
|
+
/** Shared owner comparison for attachment, binding and human-only reply gates. */
|
|
9
|
+
export declare function sameFrontendOwner({ expected: a, actual: b }: {
|
|
10
|
+
expected: FrontendSessionOwner | undefined;
|
|
11
|
+
actual: FrontendSessionOwner | undefined;
|
|
12
|
+
}, _options?: {}): boolean;
|
|
1
13
|
/** A surface that has attached itself and can execute capabilities on a bound run's behalf. */
|
|
2
14
|
export interface FrontendSessionDescriptor {
|
|
3
15
|
/** Identifies this surface. Minted by the host at attach time; opaque here. */
|
|
4
16
|
readonly sessionId: string;
|
|
17
|
+
readonly owner?: FrontendSessionOwner;
|
|
18
|
+
/** Host-authenticated quota scopes (e.g. guest and trusted client IP), never browser claims. */
|
|
19
|
+
readonly capacityKeys?: readonly string[];
|
|
5
20
|
/**
|
|
6
21
|
* Capability ids this surface claims it can execute. A call for anything this surface does not
|
|
7
22
|
* claim fails closed rather than hanging.
|
|
@@ -43,6 +58,12 @@ export interface FrontendSessionHandle {
|
|
|
43
58
|
detach(_args: Record<string, never>): void;
|
|
44
59
|
}
|
|
45
60
|
export interface FrontendSessionRegistry {
|
|
61
|
+
/** HTTP response authentication; possession of a URL session id grants nothing. */
|
|
62
|
+
ownsSession(args: {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
owner: FrontendSessionOwner;
|
|
65
|
+
bindToken: string;
|
|
66
|
+
}): boolean;
|
|
46
67
|
/**
|
|
47
68
|
* Registers a surface and the function that pushes invocations to it (an SSE write, a WebSocket
|
|
48
69
|
* send, an in-process call for a browser-resident caller that needs no relay at all).
|
|
@@ -52,6 +73,9 @@ export interface FrontendSessionRegistry {
|
|
|
52
73
|
attach(args: {
|
|
53
74
|
readonly descriptor: FrontendSessionDescriptor;
|
|
54
75
|
readonly deliver: (invocation: FrontendInvocation) => void;
|
|
76
|
+
readonly cancel?: (args: {
|
|
77
|
+
invocationId: string;
|
|
78
|
+
}) => void;
|
|
55
79
|
}): FrontendSessionHandle;
|
|
56
80
|
/**
|
|
57
81
|
* Associates a started run with the surface that originated it, so capability calls made by that
|
|
@@ -68,6 +92,13 @@ export interface FrontendSessionRegistry {
|
|
|
68
92
|
readonly runId: string;
|
|
69
93
|
readonly sessionId: string;
|
|
70
94
|
}): () => void;
|
|
95
|
+
/** Acknowledges human-wait progress only for a still-pending invocation on this session. */
|
|
96
|
+
progress(args: {
|
|
97
|
+
sessionId: string;
|
|
98
|
+
invocationId: string;
|
|
99
|
+
pending: boolean;
|
|
100
|
+
decision?: ToolApprovalDecision;
|
|
101
|
+
}): boolean;
|
|
71
102
|
/**
|
|
72
103
|
* Binds a run to whichever surface holds `bindToken` — the wire-safe form of {@link bindRun}.
|
|
73
104
|
*
|
|
@@ -86,6 +117,7 @@ export interface FrontendSessionRegistry {
|
|
|
86
117
|
bindRunByToken(args: {
|
|
87
118
|
readonly runId: string;
|
|
88
119
|
readonly bindToken: string;
|
|
120
|
+
readonly owner?: FrontendSessionOwner;
|
|
89
121
|
}): () => void;
|
|
90
122
|
/**
|
|
91
123
|
* Routes one capability call to the surface bound to `runId` and resolves with its output.
|
|
@@ -99,6 +131,7 @@ export interface FrontendSessionRegistry {
|
|
|
99
131
|
readonly input: Record<string, unknown>;
|
|
100
132
|
}, optionalArgs?: {
|
|
101
133
|
readonly signal?: AbortSignal;
|
|
134
|
+
readonly onApprovalStateChange?: ToolExecutionOptions["onApprovalStateChange"];
|
|
102
135
|
}): Promise<unknown>;
|
|
103
136
|
/**
|
|
104
137
|
* Settles a pending invocation with the surface's answer.
|
|
@@ -125,6 +158,8 @@ export interface FrontendSessionRegistry {
|
|
|
125
158
|
}): FrontendSessionDescriptor | undefined;
|
|
126
159
|
}
|
|
127
160
|
export interface CreateFrontendSessionRegistryOptions {
|
|
161
|
+
readonly maxSessions?: number;
|
|
162
|
+
readonly maxSessionsPerKey?: number;
|
|
128
163
|
/** Injectable id source for invocation ids — defaults to `randomUUID`. Test-only hook. */
|
|
129
164
|
readonly newInvocationId?: () => string;
|
|
130
165
|
/**
|
|
@@ -136,6 +171,10 @@ export interface CreateFrontendSessionRegistryOptions {
|
|
|
136
171
|
*/
|
|
137
172
|
readonly newBindToken?: () => string;
|
|
138
173
|
}
|
|
174
|
+
/** Expected admission refusal; HTTP maps it to a quiet 429 before opening SSE. */
|
|
175
|
+
export declare class FrontendSessionCapacityError extends Error {
|
|
176
|
+
constructor();
|
|
177
|
+
}
|
|
139
178
|
/**
|
|
140
179
|
* Creates an in-memory registry of attached surfaces.
|
|
141
180
|
*
|
|
@@ -143,8 +182,8 @@ export interface CreateFrontendSessionRegistryOptions {
|
|
|
143
182
|
* so it cannot outlive the process that holds it, and a restart correctly presents as "nothing
|
|
144
183
|
* attached" rather than as a stale route to a tab that is long gone.
|
|
145
184
|
*
|
|
146
|
-
* @complexity
|
|
147
|
-
* invocations and bound runs.
|
|
185
|
+
* @complexity `attach` checks O(s × k) live sessions/quota keys when limits are configured;
|
|
186
|
+
* `detach` is O(p + b) in pending invocations and bound runs. Other addressing operations are O(1).
|
|
148
187
|
*/
|
|
149
|
-
export declare function createFrontendSessionRegistry(requiredArgs: Record<string, never>, optionalArgs?: Pick<CreateFrontendSessionRegistryOptions, "newInvocationId" | "newBindToken">): FrontendSessionRegistry;
|
|
188
|
+
export declare function createFrontendSessionRegistry(requiredArgs: Record<string, never>, optionalArgs?: Pick<CreateFrontendSessionRegistryOptions, "newInvocationId" | "newBindToken" | "maxSessions" | "maxSessionsPerKey">): FrontendSessionRegistry;
|
|
150
189
|
//# sourceMappingURL=frontend-session-registry.d.ts.map
|