@xingular/harness 2.1.2 → 3.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/CHANGELOG.md +12 -0
- package/NOTICE.md +22 -0
- package/README.md +67 -36
- package/dist/cli.js +6023 -6621
- package/dist/terminal-pi.js +19 -24
- package/docs/terminal-tui.md +27 -7
- package/package.json +2 -2
- package/src/accounts/accounts.ts +4 -3
- package/src/capabilities/api.ts +1 -1
- package/src/cli.ts +7 -3
- package/src/core/caller.ts +34 -0
- package/src/core/fs.ts +17 -1
- package/src/harness.ts +55 -66
- package/src/{memory → history}/recall.test.ts +1 -1
- package/src/{memory → history}/recall.ts +1 -1
- package/src/{memory → history}/trajectory.test.ts +4 -4
- package/src/{memory → history}/trajectory.ts +1 -1
- package/src/hub/hub.test.ts +4 -5
- package/src/hub/hub.ts +2 -2
- package/src/index.ts +28 -14
- package/src/mcp/connections.ts +1 -1
- package/src/mcp/service.ts +1 -1
- package/src/pi/proxy.test.ts +68 -0
- package/src/pi/runtime.test.ts +61 -0
- package/src/pi/runtime.ts +50 -0
- package/src/plugins/api.ts +11 -9
- package/src/plugins/builtin.ts +22 -40
- package/src/plugins/catalogue.test.ts +13 -41
- package/src/plugins/catalogue.ts +3 -32
- package/src/plugins/discover.ts +2 -1
- package/src/plugins/firstParty.test.ts +1 -1
- package/src/plugins/hooks.test.ts +21 -71
- package/src/plugins/hooks.ts +6 -7
- package/src/plugins/identity.ts +16 -2
- package/src/plugins/install.test.ts +1 -1
- package/src/plugins/install.ts +2 -1
- package/src/plugins/inventory.ts +19 -18
- package/src/plugins/plugins.ts +38 -2
- package/src/prompt/compile.test.ts +82 -193
- package/src/prompt/compile.ts +98 -363
- package/src/refine/cli.ts +114 -0
- package/src/refine/demo.ts +124 -0
- package/src/refine/evidence.ts +349 -0
- package/src/refine/host.ts +102 -0
- package/src/refine/ledger.ts +104 -0
- package/src/refine/library.ts +192 -0
- package/src/refine/pi.test.ts +90 -0
- package/src/refine/pi.ts +218 -0
- package/src/refine/prompts.ts +430 -0
- package/src/refine/recursion.ts +353 -0
- package/src/refine/refine.test.ts +140 -0
- package/src/refine/runner.ts +205 -0
- package/src/refine/selection.ts +291 -0
- package/src/refine/service.ts +194 -0
- package/src/refine/trajectory.ts +71 -0
- package/src/refine/types.ts +91 -0
- package/src/server/methods.ts +137 -123
- package/src/server/server.test.ts +37 -30
- package/src/sessions/contracts.ts +6 -1
- package/src/sessions/conversation.test.ts +2 -27
- package/src/sessions/policy.ts +1 -1
- package/src/sessions/pricing.ts +3 -1
- package/src/sessions/registry.ts +5 -1
- package/src/sessions/runStore.ts +3 -1
- package/src/sessions/runner.ts +10 -5
- package/src/sessions/sessions.test.ts +45 -6
- package/src/sessions/sessions.ts +40 -17
- package/src/sessions/subagentService.test.ts +22 -1
- package/src/sessions/subagentService.ts +14 -1
- package/src/sessions/subagents.ts +2 -0
- package/src/terminal/cli.ts +24 -48
- package/src/terminal/pluginControl.test.ts +7 -8
- package/src/terminal/socket.ts +3 -3
- package/src/terminal/terminal.test.ts +21 -39
- package/src/terminal/terminals.ts +20 -13
- package/src/terminal/tui/application.test.ts +58 -0
- package/src/terminal/tui/application.ts +10 -4
- package/src/terminal/tui/clipboardFiles.test.ts +94 -0
- package/src/terminal/tui/clipboardFiles.ts +86 -0
- package/src/terminal/tui/piAdapter.ts +7 -4
- package/src/terminal/tui/runtime.ts +2 -0
- package/src/tools/definitions.ts +49 -132
- package/src/tools/dispatch.ts +78 -0
- package/src/tools/harnessTools.ts +23 -0
- package/src/tools/spawnSubagent.ts +3 -3
- package/src/tools/types.ts +1 -1
- package/src/tree/components.ts +221 -0
- package/src/tree/materialise.ts +122 -0
- package/src/tree/seed.ts +123 -0
- package/src/tree/store.ts +193 -0
- package/src/tree/surface.ts +135 -0
- package/src/tree/tree.test.ts +126 -0
- package/src/tree/tree.ts +346 -0
- package/src/tree/trust.ts +86 -0
- package/src/wiki/notes.ts +313 -0
- package/src/memory/api.test.ts +0 -82
- package/src/memory/api.ts +0 -303
- package/src/memory/blocks.test.ts +0 -78
- package/src/memory/blocks.ts +0 -408
- package/src/memory/caller.ts +0 -69
- package/src/memory/curation.test.ts +0 -206
- package/src/memory/curation.ts +0 -64
- package/src/memory/dispatch.ts +0 -64
- package/src/memory/facts.test.ts +0 -41
- package/src/memory/facts.ts +0 -248
- package/src/memory/graph.ts +0 -103
- package/src/memory/health.test.ts +0 -130
- package/src/memory/health.ts +0 -74
- package/src/memory/maintainer.ts +0 -272
- package/src/memory/maintenance.test.ts +0 -192
- package/src/memory/maintenance.ts +0 -679
- package/src/memory/migrate.ts +0 -79
- package/src/memory/operations.ts +0 -150
- package/src/memory/proposals.test.ts +0 -168
- package/src/memory/proposals.ts +0 -584
- package/src/memory/references.ts +0 -77
- package/src/memory/runContext.ts +0 -93
- package/src/memory/store.test.ts +0 -66
- package/src/memory/store.ts +0 -385
- package/src/memory/wiki.test.ts +0 -47
- package/src/memory/wiki.ts +0 -423
- package/src/memory/wikiIndex.test.ts +0 -88
- package/src/memory/wikiIndex.ts +0 -117
- package/src/plugins/proposal.ts +0 -121
- package/src/skills/registry.test.ts +0 -104
- package/src/skills/registry.ts +0 -413
- package/src/tools/maintenance.ts +0 -29
- package/src/tools/memoryTools.ts +0 -22
- package/src/tools/pluginPropose.ts +0 -41
- package/src/tools/registryWrite.test.ts +0 -101
- package/src/tools/registryWrite.ts +0 -182
- /package/src/{memory → core}/secrets.ts +0 -0
package/src/sessions/runner.ts
CHANGED
|
@@ -6,10 +6,10 @@ import type { CreateAgentSessionOptions, ModelRuntime, SettingsManager } from '@
|
|
|
6
6
|
import { classifyMessage } from '../accounts/accounts'
|
|
7
7
|
import type { AccountContext } from '../accounts/env'
|
|
8
8
|
import { HarnessError } from '../core/errors'
|
|
9
|
+
import { loadPi } from '../pi/runtime'
|
|
9
10
|
import type { InlineExtension } from '../plugins/firstParty'
|
|
10
11
|
import type { Loading, Registered } from '../plugins/plugins'
|
|
11
12
|
import { CONTEXT_MESSAGE_TYPE, CONTEXT_SNAPSHOT_KEY, type ContextUpdate } from '../prompt/compile'
|
|
12
|
-
import type { RegistryResources } from '../skills/registry'
|
|
13
13
|
import type { ToolSpec } from '../tools/types'
|
|
14
14
|
import { toolAttachmentRefsSchema, type UsageRequest } from './contracts'
|
|
15
15
|
import { completedReasoningSummary, REASONING_SUMMARY_APIS, requestConciseReasoningSummary } from './reasoningSummaries'
|
|
@@ -40,7 +40,10 @@ export interface TurnParams {
|
|
|
40
40
|
/** Unattended: no project extensions, no context files, settings held in memory. */
|
|
41
41
|
automatic?: boolean
|
|
42
42
|
account: AccountContext
|
|
43
|
-
|
|
43
|
+
/** Skills and prompt templates to load natively: the harness tree's skills folder. */
|
|
44
|
+
resources?: { ids: string[]; skillPaths: string[]; promptPaths: string[] }
|
|
45
|
+
/** Pi extensions from the harness tree (c8) that may load: trusted code, or a probe the person allowed. */
|
|
46
|
+
treeExtensions?: string[]
|
|
44
47
|
/**
|
|
45
48
|
* What the harness found for this session to load, and where it reports what loaded. Pi's own
|
|
46
49
|
* discovery is off whenever this is set, so only these paths and the registry's reach the session.
|
|
@@ -482,13 +485,15 @@ export async function runTurn(run: ActiveRun, session: PiSessionLike, params: Tu
|
|
|
482
485
|
* Loads what a session may use besides its tools: Pi's context files, the registry's skills and
|
|
483
486
|
* prompts, and what the harness found for it. `gate` is loaded after every extension on disk.
|
|
484
487
|
*/
|
|
485
|
-
export async function loadResources(pi: typeof import('@earendil-works/pi-coding-agent'), params: Pick<TurnParams, 'cwd' | 'account' | 'automatic' | 'resources' | 'plugins' | 'system' | 'inline'>, gate: unknown, automaticSettings?: SettingsManager) {
|
|
488
|
+
export async function loadResources(pi: typeof import('@earendil-works/pi-coding-agent'), params: Pick<TurnParams, 'cwd' | 'account' | 'automatic' | 'resources' | 'treeExtensions' | 'plugins' | 'system' | 'inline'>, gate: unknown, automaticSettings?: SettingsManager) {
|
|
486
489
|
const loading = params.automatic ? null : ((await params.plugins?.loading()) ?? null)
|
|
487
490
|
const resourceLoader = new pi.DefaultResourceLoader({
|
|
488
491
|
...(automaticSettings ? { settingsManager: automaticSettings, noExtensions: true, noContextFiles: true } : {}),
|
|
489
492
|
// The loader gets empty settings and no discovery of its own: it loads the paths below and
|
|
490
493
|
// nothing else. The session still reads the account's real settings for everything but resources.
|
|
491
|
-
...(loading ? { settingsManager: pi.SettingsManager.inMemory({}), noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true
|
|
494
|
+
...(loading ? { settingsManager: pi.SettingsManager.inMemory({}), noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true } : {}),
|
|
495
|
+
// The harness tree's plugins load in every kind of run, unattended ones included.
|
|
496
|
+
additionalExtensionPaths: [...(loading?.extensionPaths ?? []), ...(params.treeExtensions ?? [])],
|
|
492
497
|
cwd: params.cwd,
|
|
493
498
|
agentDir: params.account.configDir,
|
|
494
499
|
// Inline extensions load after the ones on disk, and the gate after all of them, so it sees
|
|
@@ -520,7 +525,7 @@ export function registeredBy(loader: { getExtensions(): { extensions: readonly u
|
|
|
520
525
|
|
|
521
526
|
/** Opens a Pi session for one turn: the account's runtime, the compiled prompt, and the gated tools. */
|
|
522
527
|
export const piSessionFactory: SessionFactory = async ({ params, allow, gate, tools, runtime: loadRuntime, extensions: host }) => {
|
|
523
|
-
const pi = await
|
|
528
|
+
const pi = await loadPi()
|
|
524
529
|
const { Type } = await import('typebox')
|
|
525
530
|
const runtime = await loadRuntime()
|
|
526
531
|
|
|
@@ -194,7 +194,7 @@ describe('session turns on Pi', () => {
|
|
|
194
194
|
await tick()
|
|
195
195
|
const snapshot = events.find((event) => event.type === 'context.snapshot')!.payload as { systemPrompt: string; contextUpdate: string; tools: unknown[] }
|
|
196
196
|
expect(snapshot.systemPrompt).toContain('## Context updates')
|
|
197
|
-
expect(snapshot.contextUpdate).toContain('
|
|
197
|
+
expect(snapshot.contextUpdate).toContain('[App context update')
|
|
198
198
|
expect(snapshot.tools).toEqual([{ name: 'read', description: 'Read a file', parameters: { type: 'object', properties: { path: { type: 'string' } } } }])
|
|
199
199
|
})
|
|
200
200
|
|
|
@@ -443,7 +443,7 @@ describe('session turns on Pi', () => {
|
|
|
443
443
|
expect(loadRun(harness.env.root, 'run-3').status).toBe('interrupted')
|
|
444
444
|
})
|
|
445
445
|
|
|
446
|
-
it('gives every session the
|
|
446
|
+
it('gives every session the history, Wiki and skill tools, a question tool, and the application\'s tools in order', async () => {
|
|
447
447
|
let received: Parameters<SessionFactory>[0] | null = null
|
|
448
448
|
const { run, harness } = setup(async (input) => {
|
|
449
449
|
received = input
|
|
@@ -452,13 +452,52 @@ describe('session turns on Pi', () => {
|
|
|
452
452
|
harness.sessions.provide(() => [{ name: 'react', label: 'React', description: 'React', parameters: (Type) => Type.Object({}), execute: async () => 'ok' }])
|
|
453
453
|
await run('run-5', { tools: { allow: ['react'] } })
|
|
454
454
|
await tick()
|
|
455
|
-
expect(received!.allow.slice(0,
|
|
456
|
-
expect(received!.allow).toEqual(expect.arrayContaining(['
|
|
457
|
-
expect(received!.
|
|
458
|
-
expect(
|
|
455
|
+
expect(received!.allow.slice(0, 2)).toEqual(['react', 'ask_user'])
|
|
456
|
+
expect(received!.allow).toEqual(expect.arrayContaining(['history_search', 'wiki_search', 'wiki_read', 'skill_load', 'trajectory_read']))
|
|
457
|
+
expect(received!.allow).not.toContain('wiki_write')
|
|
458
|
+
expect(received!.tools.map((tool) => tool.name)).toEqual(['history_search', 'history_read', 'trajectory_search', 'trajectory_read', 'wiki_search', 'wiki_read', 'wiki_list', 'wiki_write', 'skill_load', 'ask_user', 'spawn_subagent', 'wait_subagents', 'react'])
|
|
459
|
+
expect(await received!.tools.find((tool) => tool.name === 'skill_load')!.execute({})).toBe('You have no skills.')
|
|
459
460
|
expect(received!.params.system).toContain('# Xingular Harness context')
|
|
460
461
|
})
|
|
461
462
|
|
|
463
|
+
it('refuses, never asks, when a refinement run reaches a call that needs a person', async () => {
|
|
464
|
+
let check: ((name: string, input: unknown) => Promise<string | null>) | null = null
|
|
465
|
+
const { run, events } = setup(async (input) => {
|
|
466
|
+
check = input.gate.check
|
|
467
|
+
return fakeSession(async () => undefined, input.gate)
|
|
468
|
+
})
|
|
469
|
+
await run('probe-1', { run: { id: 'probe-1', sessionId: 'agent-1', trigger: 'probe' }, tools: { allow: ['read', 'bash'], gated: ['bash'] }, automatic: true })
|
|
470
|
+
await tick()
|
|
471
|
+
expect(await check!('bash', { command: 'rm -rf /' })).toMatch(/^Refused in a refinement run/)
|
|
472
|
+
expect(events.some((event) => event.type === 'approval.requested')).toBe(false)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('runs on a pinned harness tree: its sections, its skills, and its version on the run', async () => {
|
|
476
|
+
let received: Parameters<SessionFactory>[0] | null = null
|
|
477
|
+
const { run, harness } = setup(async (input) => {
|
|
478
|
+
received = input
|
|
479
|
+
return fakeSession(async () => undefined, input.gate)
|
|
480
|
+
})
|
|
481
|
+
const { commit, head } = await import('../tree/store')
|
|
482
|
+
commit(harness.env.root, 'agent-1', {
|
|
483
|
+
'systemprompt.md': 'You review pull requests.\n',
|
|
484
|
+
'guardrails.md': '- When a diff is large, load the `triage` skill.\n',
|
|
485
|
+
'memory.md': '',
|
|
486
|
+
'tool_guidance.md': '',
|
|
487
|
+
'skills/triage/SKILL.md': '---\nname: triage\ndescription: a diff is large\n---\nSplit it by folder.\n',
|
|
488
|
+
'plugins/guard.ts': 'export default function (pi) {}\n'
|
|
489
|
+
}, { kind: 'seed' })
|
|
490
|
+
await run('run-tree', { harness: { scope: 'agent-1' } })
|
|
491
|
+
await tick()
|
|
492
|
+
const record = loadRun(harness.env.root, 'run-tree')
|
|
493
|
+
expect(record).toMatchObject({ harnessScope: 'agent-1', harnessVersion: head(harness.env.root, 'agent-1')!.fingerprint })
|
|
494
|
+
expect(received!.params.system).toContain('You review pull requests.')
|
|
495
|
+
expect(received!.params.resources!.skillPaths[0]).toMatch(/versions\/[0-9a-f]{16}\/tree\/skills$/)
|
|
496
|
+
// Plugin code waits for trust.
|
|
497
|
+
expect(received!.params.treeExtensions).toEqual([])
|
|
498
|
+
expect(await received!.tools.find((tool) => tool.name === 'skill_load')!.execute({ name: 'triage' })).toContain('Split it by folder.')
|
|
499
|
+
})
|
|
500
|
+
|
|
462
501
|
it('refuses missing continuation history before creating a run or opening a session', async () => {
|
|
463
502
|
let called = false
|
|
464
503
|
const { run, events, harness } = setup(async ({ gate }) => {
|
package/src/sessions/sessions.ts
CHANGED
|
@@ -12,18 +12,18 @@ import { capabilitySet as parseCapabilitySet, freezeRun } from '../capabilities/
|
|
|
12
12
|
import type { CapabilitySet } from '../capabilities/types'
|
|
13
13
|
import { now, writeJson } from '../core/fs'
|
|
14
14
|
import { takeChars } from '../core/text'
|
|
15
|
-
import type { CallerContext } from '../
|
|
16
|
-
import type { DispatchEnv } from '../
|
|
15
|
+
import type { CallerContext } from '../core/caller'
|
|
16
|
+
import type { DispatchEnv } from '../tools/dispatch'
|
|
17
17
|
import type { Plugins } from '../plugins/plugins'
|
|
18
18
|
import { sessionLaunch, type Launch } from '../prompt/compile'
|
|
19
19
|
import * as attachments from '../plugins/attachments'
|
|
20
20
|
import { FIRST_PARTY_PLUGINS, firstPartyExtensions, type InlineExtension } from '../plugins/firstParty'
|
|
21
|
-
import
|
|
21
|
+
import { materialise, type HarnessChoice, type Materialised } from '../tree/materialise'
|
|
22
|
+
import type { TreeTrust } from '../tree/trust'
|
|
22
23
|
import { ASK_USER, askUserTool } from '../tools/askUser'
|
|
23
|
-
import { PLUGIN_PROPOSE, pluginProposeTool } from '../tools/pluginPropose'
|
|
24
24
|
import { subagentTools } from '../tools/spawnSubagent'
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { DEFAULT_TOOL_NAMES } from '../tools/definitions'
|
|
26
|
+
import { harnessTools } from '../tools/harnessTools'
|
|
27
27
|
import type { ToolProvider, ToolSession, ToolSpec } from '../tools/types'
|
|
28
28
|
import type { RunRecord } from './contracts'
|
|
29
29
|
import { SPAWN_SUBAGENT, WAIT_SUBAGENTS, subagentDepth, subagentHistory, subagentNesting, type SubagentInput, type SubagentOptions } from './subagents'
|
|
@@ -58,8 +58,13 @@ export interface SessionRunInput {
|
|
|
58
58
|
preamble?: string | null
|
|
59
59
|
/** Unattended: no project extensions, no context files, nothing may ask a person. */
|
|
60
60
|
automatic?: boolean
|
|
61
|
-
/** Who the session works as, for
|
|
61
|
+
/** Who the session works as, for history, the Wiki and the prompt. */
|
|
62
62
|
caller: CallerContext
|
|
63
|
+
/**
|
|
64
|
+
* The harness tree the session runs: a scope's head (or a version of it), or a tree on disk.
|
|
65
|
+
* Its version is pinned for the whole run and recorded on it.
|
|
66
|
+
*/
|
|
67
|
+
harness?: HarnessChoice | null
|
|
63
68
|
/** What the application adds to the prompt, and which tools it describes. */
|
|
64
69
|
launch?: Launch
|
|
65
70
|
/** The tools switched on, in the order the model sees them, and those that ask before each call. */
|
|
@@ -74,6 +79,8 @@ export interface SessionRunInput {
|
|
|
74
79
|
subagents?: SubagentOptions | null
|
|
75
80
|
/** Ask the application's tool providers for tools (the default). False runs with the harness's own tools only. */
|
|
76
81
|
providers?: boolean
|
|
82
|
+
/** Give the session the harness's own tools: History, the Wiki, skills, and asking the user (the default). */
|
|
83
|
+
harnessTools?: boolean
|
|
77
84
|
/**
|
|
78
85
|
* Plan mode: the run may only read, search, recall and ask, and is told to end with a
|
|
79
86
|
* `<proposed_plan>` block. Other calls are refused at the gate; the tool list is unchanged.
|
|
@@ -99,6 +106,8 @@ export interface SessionsOptions {
|
|
|
99
106
|
/** Resolves the caller's capability set from the harness's stores. */
|
|
100
107
|
resolveCapabilities?: (caller: CallerContext, tools: readonly string[], cwd: string) => Promise<CapabilitySet>
|
|
101
108
|
factory?: SessionFactory
|
|
109
|
+
/** Which plugin code in harness trees a person trusted. */
|
|
110
|
+
trust?: TreeTrust | null
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
/** The harness's own plugins a person switched on for this caller, with the numbers they set. */
|
|
@@ -142,6 +151,8 @@ export class Sessions {
|
|
|
142
151
|
return error instanceof Error ? error.message : 'Tool unavailable in the app settings.'
|
|
143
152
|
}
|
|
144
153
|
if (scope.allowed && !scope.humanOnly && (scope.automatic || !gated.has(tool))) return null
|
|
154
|
+
// Refinement runs have nobody to ask: what would need a person is refused, never queued.
|
|
155
|
+
if (call.run.trigger === 'probe' || call.run.trigger === 'improver') return scope.reason ? `Refused in a refinement run: ${scope.reason}` : 'Refused in a refinement run: this call needs a person to approve it.'
|
|
145
156
|
// A call outside the scope is approved one at a time; a gated tool may be remembered, unless only a person may decide it.
|
|
146
157
|
const decision = await run.requestApproval({
|
|
147
158
|
target: tool,
|
|
@@ -178,15 +189,13 @@ export class Sessions {
|
|
|
178
189
|
const account = this.options.accounts.resolve(input.accountId, input.poolId)
|
|
179
190
|
const context = this.options.accounts.context(account)
|
|
180
191
|
const runId = input.run.id
|
|
181
|
-
const caller: CallerContext = { ...input.caller,
|
|
192
|
+
const caller: CallerContext = { ...input.caller, runId, sessionId: input.caller.sessionId ?? input.run.sessionId }
|
|
182
193
|
const broker = input.connections ? this.options.broker : null
|
|
183
194
|
const depth = input.depth ?? 0
|
|
184
195
|
const canSpawn = input.subagents !== null && (!input.automatic || input.subagents !== undefined) && subagentNesting(root, input.run) < subagentDepth(input.subagents ?? undefined)
|
|
185
196
|
const requestedTools = [...new Set([
|
|
186
197
|
...input.tools.allow.filter((name) => name !== SPAWN_SUBAGENT && name !== WAIT_SUBAGENTS),
|
|
187
|
-
ASK_USER,
|
|
188
|
-
...(input.automatic ? [] : [PLUGIN_PROPOSE]),
|
|
189
|
-
...WIKI_TOOL_NAMES,
|
|
198
|
+
...(input.harnessTools === false ? [] : [ASK_USER, ...DEFAULT_TOOL_NAMES]),
|
|
190
199
|
...(broker?.toolNames ?? []),
|
|
191
200
|
...(canSpawn ? [SPAWN_SUBAGENT, WAIT_SUBAGENTS] : [])
|
|
192
201
|
])]
|
|
@@ -202,6 +211,16 @@ export class Sessions {
|
|
|
202
211
|
}
|
|
203
212
|
freezeRun(root, runId, caller, capabilities)
|
|
204
213
|
|
|
214
|
+
// The harness is resolved once and pinned: a child run or a later turn of this run uses the
|
|
215
|
+
// same files even if the head moves meanwhile.
|
|
216
|
+
let tree: Materialised | null = null
|
|
217
|
+
if (input.harness) {
|
|
218
|
+
const scope = input.harness.scope ?? null
|
|
219
|
+
const trust = this.options.trust ?? null
|
|
220
|
+
tree = materialise(root, input.harness, { trust, probeCode: input.run.trigger === 'probe' && Boolean(scope && trust?.probeCode(scope)), subagents: capabilities.tools.includes(SPAWN_SUBAGENT) })
|
|
221
|
+
input.harness = { dir: tree.dir, scope: tree.scope }
|
|
222
|
+
}
|
|
223
|
+
|
|
205
224
|
saveRun(root, {
|
|
206
225
|
resumedFromRunId: input.run.resumedFromRunId ?? null,
|
|
207
226
|
parentRunId: input.run.parentRunId ?? null,
|
|
@@ -225,7 +244,9 @@ export class Sessions {
|
|
|
225
244
|
handoffId: input.run.handoffId ?? null,
|
|
226
245
|
accountId: account.id,
|
|
227
246
|
agentId: input.run.agentId ?? caller.agentId ?? null,
|
|
228
|
-
trigger: input.run.trigger ?? null
|
|
247
|
+
trigger: input.run.trigger ?? null,
|
|
248
|
+
harnessScope: tree?.scope ?? null,
|
|
249
|
+
harnessVersion: tree?.fingerprint ?? null
|
|
229
250
|
})
|
|
230
251
|
|
|
231
252
|
// What the session is, kept with the run so later consolidation knows which memory it had.
|
|
@@ -254,7 +275,7 @@ export class Sessions {
|
|
|
254
275
|
connectionTools: (launch.connectionTools ?? []).filter((name) => capabilities.tools.includes(name)),
|
|
255
276
|
toolGroups: launch.toolGroups?.map((group) => ({ ...group, tools: group.tools.filter((name) => capabilities.tools.includes(name)) })).filter((group) => group.tools.length > 0)
|
|
256
277
|
}
|
|
257
|
-
;({ system, update } = sessionLaunch(this.env, caller, effectiveLaunch, historyFile,
|
|
278
|
+
;({ system, update } = sessionLaunch(this.env, caller, effectiveLaunch, historyFile, tree))
|
|
258
279
|
} catch (error) {
|
|
259
280
|
fail(error)
|
|
260
281
|
throw error
|
|
@@ -262,6 +283,9 @@ export class Sessions {
|
|
|
262
283
|
|
|
263
284
|
const run = this.options.runs.start(input.run.sessionId, runId)
|
|
264
285
|
if (run.finished) return { runId, nativeSessionId: null }
|
|
286
|
+
if (tree?.untrustedPlugins.length) {
|
|
287
|
+
run.emit('extension.notice', { kind: 'notify', level: 'warning', text: `Harness plugins not loaded until their code is trusted: ${tree.untrustedPlugins.join(', ')}.` })
|
|
288
|
+
}
|
|
265
289
|
// What every session has, after the tools the caller switched on and in a fixed order.
|
|
266
290
|
const allow = capabilities.tools
|
|
267
291
|
const session: ToolSession = { run, caller, allow, depth, cwd: input.cwd, capabilities }
|
|
@@ -271,9 +295,7 @@ export class Sessions {
|
|
|
271
295
|
try {
|
|
272
296
|
const tools: ToolSpec[] = [
|
|
273
297
|
...(broker ? broker.tools(session, { readOnly: typeof input.connections === 'object' && input.connections.readOnly === true }) : []),
|
|
274
|
-
...
|
|
275
|
-
askUserTool(run),
|
|
276
|
-
pluginProposeTool(this.options.env, caller),
|
|
298
|
+
...(input.harnessTools === false ? [] : [...harnessTools(this.options.env, caller, { treeDir: tree?.dir ?? null }), askUserTool(run)]),
|
|
277
299
|
...subagentTools(this.options.subagents, runId).filter(tool => allow.includes(tool.name))
|
|
278
300
|
]
|
|
279
301
|
if (input.providers !== false) for (const provider of this.providers) tools.push(...(await provider(session)))
|
|
@@ -294,7 +316,8 @@ export class Sessions {
|
|
|
294
316
|
preamble: input.preamble ?? null,
|
|
295
317
|
automatic: input.automatic,
|
|
296
318
|
account: context,
|
|
297
|
-
resources:
|
|
319
|
+
resources: { ids: [], skillPaths: tree?.skillPaths ?? [], promptPaths: [] },
|
|
320
|
+
treeExtensions: tree?.extensionPaths ?? [],
|
|
298
321
|
inline: [...firstPartyFor(root, capabilities.contributions), runLineageJoin(root, runId), this.options.subagents.extension(runId)],
|
|
299
322
|
...(plugins ? { plugins: { loading: () => plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }), loaded: (extensions) => plugins.loaded(extensions) } } : {}),
|
|
300
323
|
system,
|
|
@@ -14,8 +14,10 @@ afterEach(async () => { for (const harness of open.splice(0)) await harness.clos
|
|
|
14
14
|
|
|
15
15
|
function setup() {
|
|
16
16
|
const finish = new Map<string, (text: string) => void>()
|
|
17
|
+
const systems = new Map<string, string>()
|
|
17
18
|
const root = mkdtempSync(join(tmpdir(), 'native-subagents-'))
|
|
18
19
|
const harness = createHarness({ dataRoot: join(root, 'data'), stateDir: join(root, 'state'), secrets: memorySecretStore(), sessionFactory: async ({ params }) => {
|
|
20
|
+
systems.set(params.runId, params.system)
|
|
19
21
|
let emit: (event: any) => void = () => {}
|
|
20
22
|
let done: () => void = () => {}
|
|
21
23
|
return { sessionId: 'native-history', subscribe: (listener) => { emit = listener; return () => {} }, sendCustomMessage: async () => {},
|
|
@@ -29,10 +31,29 @@ function setup() {
|
|
|
29
31
|
run: { id, sessionId: 'opaque-parent' }, caller: { agentId: 'caller', actor: 'Chat' }, cwd: root,
|
|
30
32
|
sessionDir: join(root, 'history'), text: 'Parent', tools: { allow: ['read'] }, ...extra
|
|
31
33
|
})
|
|
32
|
-
return { harness, finish, start }
|
|
34
|
+
return { harness, finish, start, systems }
|
|
33
35
|
}
|
|
34
36
|
const empty = () => ({ owners: [], receipts: [] })
|
|
35
37
|
|
|
38
|
+
describe('subagents a harness defines (c5)', () => {
|
|
39
|
+
it('starts one by name with its own instructions instead of the parent\'s, and refuses a name the harness lacks', async () => {
|
|
40
|
+
const { harness, start, systems } = setup()
|
|
41
|
+
const { commit } = await import('../tree/store')
|
|
42
|
+
commit(harness.env.root, 'caller', { 'systemprompt.md': 'You coordinate releases.\n', 'guardrails.md': '', 'memory.md': '', 'tool_guidance.md': '', 'sub_agents/reader/AGENT.md': '---\ndescription: reads long logs and answers briefly\n---\nRead the log and answer in three lines.\n' }, { kind: 'seed' })
|
|
43
|
+
await start('parent', { harness: { scope: 'caller' }, tools: { allow: ['read', 'spawn_subagent', 'wait_subagents'] } })
|
|
44
|
+
expect(systems.get('parent')).toContain('You coordinate releases.')
|
|
45
|
+
expect(systems.get('parent')).toContain('- reader — reads long logs and answers briefly')
|
|
46
|
+
const child = await harness.subagents.spawn({ parentRunId: 'parent', brief: 'Summarise build.log', agent: 'reader' })
|
|
47
|
+
expect(systems.get(child.runId)).toContain('Read the log and answer in three lines.')
|
|
48
|
+
expect(systems.get(child.runId)).not.toContain('You coordinate releases.')
|
|
49
|
+
// A plain child keeps the parent's harness, pinned to the same version.
|
|
50
|
+
const copy = await harness.subagents.spawn({ parentRunId: 'parent', brief: 'Check the tests' })
|
|
51
|
+
expect(systems.get(copy.runId)).toContain('You coordinate releases.')
|
|
52
|
+
expect(loadRun(harness.env.root, copy.runId).harnessVersion).toBe(loadRun(harness.env.root, 'parent').harnessVersion)
|
|
53
|
+
await expect(harness.subagents.spawn({ parentRunId: 'parent', brief: 'x', agent: 'writer' })).rejects.toThrow('There is no subagent named writer.')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
36
57
|
describe('native subagent ownership and delivery', () => {
|
|
37
58
|
it('spawns asynchronously, joins selected children, and rejects foreign handles', async () => {
|
|
38
59
|
const { harness, finish, start } = setup()
|
|
@@ -13,6 +13,7 @@ import type { RunRegistry } from './registry'
|
|
|
13
13
|
import type { Effort } from './runner'
|
|
14
14
|
import type { SessionRunInput } from './sessions'
|
|
15
15
|
import { isActiveStatus } from './contracts'
|
|
16
|
+
import { subagentOf } from '../tree/materialise'
|
|
16
17
|
import { findRun, loadRun, readEvents, recordedSubagents, recordSubagent, saveRun } from './runStore'
|
|
17
18
|
import { SPAWN_SUBAGENT, SUBAGENT_RELATION, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory, type SubagentInput, type SubagentReceipt, type SubagentResult, type SubagentSelection } from './subagents'
|
|
18
19
|
|
|
@@ -104,6 +105,17 @@ export class Subagents {
|
|
|
104
105
|
if (outside.length) throw invalid(`A subagent cannot have tools you do not have: ${outside.join(', ')}.`)
|
|
105
106
|
const tools = capabilities.tools.filter((name) => !request.tools || request.tools.includes(name))
|
|
106
107
|
await until(parent.ready, parent.stopped.signal)
|
|
108
|
+
let harness = input.harness ?? null
|
|
109
|
+
let launch = input.launch
|
|
110
|
+
if (request.agent !== undefined) {
|
|
111
|
+
if (typeof request.agent !== 'string' || !request.agent.trim()) throw invalid('agent must be the name of one of your subagents.')
|
|
112
|
+
const dir = harness && 'dir' in harness ? harness.dir : null
|
|
113
|
+
const definition = dir ? subagentOf(dir, request.agent.trim()) : null
|
|
114
|
+
if (!definition) throw invalid(`There is no subagent named ${request.agent.trim()}.`)
|
|
115
|
+
// A named subagent works from its own definition, not from the parent's instructions.
|
|
116
|
+
harness = null
|
|
117
|
+
launch = { ...(input.launch ?? {}), sections: [...(input.launch?.sections ?? []).filter((section) => section.id !== 'subagent'), { id: 'subagent', title: `Your role: ${definition.name}`, body: definition.instructions || definition.description, budget: 16_000, order: 22 }] }
|
|
118
|
+
}
|
|
107
119
|
const runId = randomUUID()
|
|
108
120
|
const sessionId = randomUUID()
|
|
109
121
|
recordSubagent(this.env.root, request.parentRunId, runId)
|
|
@@ -114,7 +126,8 @@ export class Subagents {
|
|
|
114
126
|
text: brief, images: undefined, requiresImageInput: false, preamble: null,
|
|
115
127
|
caller: { ...input.caller, runId: undefined, sessionId },
|
|
116
128
|
tools: { allow: tools, gated: (input.tools.gated ?? []).filter((name) => tools.includes(name)) },
|
|
117
|
-
capabilities: { ...capabilities, tools }, depth: (input.depth ?? 0) + 1
|
|
129
|
+
capabilities: { ...capabilities, tools }, depth: (input.depth ?? 0) + 1,
|
|
130
|
+
harness, launch
|
|
118
131
|
}
|
|
119
132
|
const started = this.launch(child).catch((error) => {
|
|
120
133
|
if (!findRun(this.env.root, runId)) {
|
package/src/terminal/cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// `xagent-wiki` and `xagent-mcp`: the harness's
|
|
2
|
-
// for a `pi` terminal session's shell. Each command maps onto one call to the session socket.
|
|
3
|
-
import type {
|
|
1
|
+
// `xagent-wiki` and `xagent-mcp`: the harness's notes, skills, history and connected apps as
|
|
2
|
+
// commands, for a `pi` terminal session's shell. Each command maps onto one call to the session socket.
|
|
3
|
+
import type { HarnessCall } from '../tools/definitions'
|
|
4
4
|
import { connectionFromEnv, socketCall } from './client'
|
|
5
5
|
|
|
6
6
|
export const WIKI_COMMAND = 'xagent-wiki'
|
|
@@ -8,22 +8,14 @@ export const APPS_COMMAND = 'xagent-mcp'
|
|
|
8
8
|
|
|
9
9
|
export const USAGE = `Usage: ${WIKI_COMMAND} <command> [arguments]
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
skill [name] Load a skill, or list skills
|
|
17
|
-
facts [query] [--namespace NS] Recall facts (also: memory)
|
|
11
|
+
search <query> [--project SLUG] Search the Wiki's project notes
|
|
12
|
+
read <path> Read one note, e.g. projects/<slug>/decisions
|
|
13
|
+
list [project slug | library] List notes
|
|
14
|
+
write <path> <text | -> Save a note (when this session may write the Wiki)
|
|
15
|
+
skill [name] Load one of your skills, or list them
|
|
18
16
|
history <query> [--scope all] [--since YYYY-MM-DD]
|
|
19
17
|
Search earlier conversations and runs (also: recall)
|
|
20
18
|
history-read <run id> Read one earlier run (also: recall-read)
|
|
21
|
-
block append <label> <text> Add to a memory block (user, project, or a shared block)
|
|
22
|
-
block replace <label> --old <text> --new <text>
|
|
23
|
-
Replace one exact passage in a memory block
|
|
24
|
-
block rethink <label> <text> Rewrite a whole memory block
|
|
25
|
-
propose <json | -> Record knowledge, e.g. {"kind":"fact","title":"…","body":"…",
|
|
26
|
-
"type":"preference","sourceRefs":[{"kind":"run","ref":"current"}]}
|
|
27
19
|
`
|
|
28
20
|
|
|
29
21
|
export const APPS_USAGE = `Usage: ${APPS_COMMAND} <command>
|
|
@@ -33,15 +25,15 @@ export const APPS_USAGE = `Usage: ${APPS_COMMAND} <command>
|
|
|
33
25
|
Use one of them
|
|
34
26
|
`
|
|
35
27
|
|
|
36
|
-
export type CliCommand = { kind: 'call'; call:
|
|
28
|
+
export type CliCommand = { kind: 'call'; call: HarnessCall; params: Record<string, unknown> } | { kind: 'help' } | { kind: 'stdin-write'; path: string }
|
|
37
29
|
|
|
38
30
|
function options(args: string[]): { positional: string[]; flags: Record<string, string> } {
|
|
39
31
|
const positional: string[] = []
|
|
40
32
|
const flags: Record<string, string> = {}
|
|
41
33
|
for (let index = 0; index < args.length; index += 1) {
|
|
42
|
-
const arg = args[index]
|
|
34
|
+
const arg = args[index]!
|
|
43
35
|
if (arg.startsWith('--') && index + 1 < args.length) {
|
|
44
|
-
flags[arg.slice(2)] = args[index + 1]
|
|
36
|
+
flags[arg.slice(2)] = args[index + 1]!
|
|
45
37
|
index += 1
|
|
46
38
|
} else {
|
|
47
39
|
positional.push(arg)
|
|
@@ -55,21 +47,23 @@ export function parseCli(argv: string[]): CliCommand {
|
|
|
55
47
|
const { positional, flags } = options(rest)
|
|
56
48
|
const joined = positional.join(' ').trim()
|
|
57
49
|
switch (command) {
|
|
58
|
-
case 'entrypoint':
|
|
59
|
-
return { kind: 'call', call: 'wiki.entrypoint', params: {} }
|
|
60
50
|
case 'search':
|
|
61
51
|
if (!joined) return { kind: 'help' }
|
|
62
|
-
return { kind: 'call', call: 'wiki.search', params: { query: joined, ...(flags.
|
|
52
|
+
return { kind: 'call', call: 'wiki.search', params: { query: joined, ...(flags.project ? { project: flags.project } : {}) } }
|
|
63
53
|
case 'read':
|
|
64
54
|
if (!joined) return { kind: 'help' }
|
|
65
|
-
return { kind: 'call', call: 'wiki.read', params: {
|
|
55
|
+
return { kind: 'call', call: 'wiki.read', params: { path: joined } }
|
|
66
56
|
case 'list':
|
|
67
|
-
return { kind: 'call', call: 'wiki.list', params:
|
|
57
|
+
return { kind: 'call', call: 'wiki.list', params: joined ? { project: joined } : {} }
|
|
58
|
+
case 'write': {
|
|
59
|
+
const [path, ...words] = positional
|
|
60
|
+
if (!path) return { kind: 'help' }
|
|
61
|
+
if (words.join(' ').trim() === '-') return { kind: 'stdin-write', path }
|
|
62
|
+
if (!words.length) return { kind: 'help' }
|
|
63
|
+
return { kind: 'call', call: 'wiki.write', params: { path, text: words.join(' ') } }
|
|
64
|
+
}
|
|
68
65
|
case 'skill':
|
|
69
|
-
return { kind: 'call', call: '
|
|
70
|
-
case 'facts':
|
|
71
|
-
case 'memory':
|
|
72
|
-
return { kind: 'call', call: 'wiki.memory', params: { ...(joined ? { query: joined } : {}), ...(flags.namespace ? { namespace: flags.namespace } : {}) } }
|
|
66
|
+
return { kind: 'call', call: 'skill.load', params: joined ? { name: joined } : {} }
|
|
73
67
|
case 'history':
|
|
74
68
|
case 'recall':
|
|
75
69
|
if (!joined) return { kind: 'help' }
|
|
@@ -78,24 +72,6 @@ export function parseCli(argv: string[]): CliCommand {
|
|
|
78
72
|
case 'recall-read':
|
|
79
73
|
if (!joined) return { kind: 'help' }
|
|
80
74
|
return { kind: 'call', call: 'recall.read', params: { run: joined } }
|
|
81
|
-
case 'block': {
|
|
82
|
-
const [action, label, ...words] = positional
|
|
83
|
-
const body = words.join(' ').trim()
|
|
84
|
-
if (action === 'append' && label && body) return { kind: 'call', call: 'memory.append', params: { block: label, text: body } }
|
|
85
|
-
if (action === 'rethink' && label && body) return { kind: 'call', call: 'memory.rethink', params: { block: label, body } }
|
|
86
|
-
if (action === 'replace' && label && flags.old !== undefined && flags.new !== undefined) return { kind: 'call', call: 'memory.replace', params: { block: label, old: flags.old, new: flags.new } }
|
|
87
|
-
return { kind: 'help' }
|
|
88
|
-
}
|
|
89
|
-
case 'propose': {
|
|
90
|
-
if (joined === '-') return { kind: 'stdin-propose' }
|
|
91
|
-
try {
|
|
92
|
-
const params = JSON.parse(joined) as unknown
|
|
93
|
-
if (params && typeof params === 'object' && !Array.isArray(params)) return { kind: 'call', call: 'wiki.propose', params: params as Record<string, unknown> }
|
|
94
|
-
} catch {
|
|
95
|
-
// Falls through to help.
|
|
96
|
-
}
|
|
97
|
-
return { kind: 'help' }
|
|
98
|
-
}
|
|
99
75
|
default:
|
|
100
76
|
return { kind: 'help' }
|
|
101
77
|
}
|
|
@@ -119,8 +95,8 @@ export async function runWikiCli(argv: string[]): Promise<number> {
|
|
|
119
95
|
try {
|
|
120
96
|
const connection = connectionFromEnv()
|
|
121
97
|
const result =
|
|
122
|
-
command.kind === 'stdin-
|
|
123
|
-
? await socketCall<string>(connection, 'wiki.
|
|
98
|
+
command.kind === 'stdin-write'
|
|
99
|
+
? await socketCall<string>(connection, 'wiki.write', { path: command.path, text: await readStdin() })
|
|
124
100
|
: await socketCall<string>(connection, command.call, command.params)
|
|
125
101
|
process.stdout.write(`${typeof result === 'string' ? result : JSON.stringify(result)}\n`)
|
|
126
102
|
return 0
|
|
@@ -80,21 +80,20 @@ describe('plugin control from a terminal session', () => {
|
|
|
80
80
|
const { key } = await socketCall<{ key: string }>(connection, 'control.claim')
|
|
81
81
|
const call = <T>(method: string, params?: Record<string, unknown>) => socketCall<T>(connection, method, params ?? {}, 5_000, key)
|
|
82
82
|
const rows = menuRows(await call<Plugin[]>('plugins.list'), 'p1')
|
|
83
|
-
expect(rows.map((row) => row.label)).toEqual(['● review-kit · code not trusted', '○
|
|
83
|
+
expect(rows.map((row) => row.label)).toEqual(['● review-kit · code not trusted', '○ Budget guard · runs code', '○ Output trim · runs code'])
|
|
84
84
|
expect(rowActions(rows[0], 'p1')).toEqual(['Switch off for this project', 'Switch off everywhere', 'Trust its code', 'Back'])
|
|
85
85
|
expect(rowActions(rows[1], null)).toEqual(['Switch on everywhere', 'Back'])
|
|
86
86
|
// A part that ships with the harness needs no trust: it is the harness's own code.
|
|
87
|
-
expect(rowActions(rows[
|
|
87
|
+
expect(rowActions(rows[1], 'p1')).toEqual(['Switch on for this project', 'Switch on everywhere', 'Back'])
|
|
88
88
|
|
|
89
|
-
// Declining the trust question leaves the code untrusted; then they trust it
|
|
90
|
-
const { ui, shown, said } = person(['● review-kit · code not trusted', 'Trust its code', false, '● review-kit · code not trusted', 'Trust its code', true, '
|
|
89
|
+
// Declining the trust question leaves the code untrusted; then they trust it and leave.
|
|
90
|
+
const { ui, shown, said } = person(['● review-kit · code not trusted', 'Trust its code', false, '● review-kit · code not trusted', 'Trust its code', true, 'Done'])
|
|
91
91
|
await runPluginsMenu(ui, call, 'p1')
|
|
92
92
|
expect(shown.find((line) => line.startsWith('Trust this code?'))).toContain('extensions/guard.ts')
|
|
93
93
|
expect(shown.find((line) => line.startsWith('Trust this code?'))).toContain('can do anything this app can')
|
|
94
94
|
expect(said).toEqual(['Saved. It applies when this session restarts.'])
|
|
95
95
|
const after = await call<Plugin[]>('plugins.list')
|
|
96
96
|
expect(after.find((plugin) => plugin.id === 'package:review-kit')).toMatchObject({ trust: 'trusted' })
|
|
97
|
-
expect(after.flatMap((plugin) => plugin.contributions).find((entry) => entry.id === 'yours/rules/tests')).toMatchObject({ state: 'on', scope: 'App' })
|
|
98
97
|
expect(harness.plugins.trust.list()).toHaveLength(1)
|
|
99
98
|
|
|
100
99
|
// Leaving at once changes nothing and says nothing.
|
|
@@ -113,7 +112,7 @@ describe('the plugins command', () => {
|
|
|
113
112
|
const last = (): string => lines.at(-1) ?? ''
|
|
114
113
|
|
|
115
114
|
expect(await run(['list'])).toBe(0)
|
|
116
|
-
expect(last()).toMatch(/^
|
|
115
|
+
expect(last()).toMatch(/^harness\s+required\s+Harness$/m)
|
|
117
116
|
expect(await run(['install', join(base, 'kit')])).toBe(0)
|
|
118
117
|
expect(last()).toContain('Installed, switched off.')
|
|
119
118
|
expect(last()).toContain('trust its code with "plugins trust <id>"')
|
|
@@ -134,8 +133,8 @@ describe('the plugins command', () => {
|
|
|
134
133
|
expect(last()).toContain('Code (trusted)')
|
|
135
134
|
expect(last()).toContain('● skill audit (package:review-kit/skill/audit)')
|
|
136
135
|
|
|
137
|
-
expect(await run(['detach', '
|
|
138
|
-
expect(last()).toContain('
|
|
136
|
+
expect(await run(['detach', 'harness'])).toBe(1)
|
|
137
|
+
expect(last()).toContain('Harness cannot be switched off.')
|
|
139
138
|
expect(await run(['untrust', 'package:review-kit'])).toBe(0)
|
|
140
139
|
expect(await run(['remove', 'package:review-kit'])).toBe(0)
|
|
141
140
|
expect(await run(['show', 'package:review-kit'])).toBe(1)
|
package/src/terminal/socket.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { log } from '../core/bus'
|
|
|
13
13
|
import { HarnessError, toErrorShape } from '../core/errors'
|
|
14
14
|
import { newId } from '../core/fs'
|
|
15
15
|
import type { BrokerSession } from '../mcp/service'
|
|
16
|
-
import type { CallerContext } from '../
|
|
16
|
+
import type { CallerContext } from '../core/caller'
|
|
17
17
|
|
|
18
18
|
/** What a token stands for. */
|
|
19
19
|
export interface SocketGrant {
|
|
@@ -27,7 +27,7 @@ export interface SocketGrant {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface SocketHandlers {
|
|
30
|
-
/** `wiki.*`, `
|
|
30
|
+
/** `wiki.*`, `skill.*`, `recall.*`, `trajectory.*`. */
|
|
31
31
|
memory(caller: CallerContext, call: string, params: Record<string, unknown>): string
|
|
32
32
|
/** `mcp.list`, `mcp.call`, `mcp.revision`, `tools.list`, `tools.call`. */
|
|
33
33
|
apps(session: BrokerSession, call: string, params: Record<string, unknown>): Promise<unknown>
|
|
@@ -147,7 +147,7 @@ export class SessionSocket {
|
|
|
147
147
|
}
|
|
148
148
|
const call = typeof request.call === 'string' ? request.call : ''
|
|
149
149
|
const params = (request.params && typeof request.params === 'object' ? request.params : {}) as Record<string, unknown>
|
|
150
|
-
if (['wiki.', '
|
|
150
|
+
if (['wiki.', 'skill.', 'recall.', 'trajectory.'].some((prefix) => call.startsWith(prefix))) return { result: this.handlers.memory(grant.caller(), call, params) }
|
|
151
151
|
if (call === 'tools.list' && !grant.broker) return { result: { tools: [] } }
|
|
152
152
|
if (['mcp.', 'tools.'].some((prefix) => call.startsWith(prefix))) {
|
|
153
153
|
if (!grant.broker) throw new HarnessError('mcp_unauthorized', 'Connected apps are not available to this session.')
|