@namzu/sdk 57.1.0 → 58.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 +51 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/file-definitions.d.ts +35 -4
- package/dist/agents/file-definitions.d.ts.map +1 -1
- package/dist/agents/file-definitions.js +98 -8
- package/dist/agents/file-definitions.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +9 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -1
- package/dist/authorization/protected-paths.js +29 -3
- package/dist/authorization/protected-paths.js.map +1 -1
- package/dist/connector/mcp/client.d.ts +8 -0
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +75 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/constants/mcp/index.d.ts +2 -0
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +2 -0
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +2 -0
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +2 -0
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +25 -4
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +2 -1
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +2 -2
- package/dist/persona/assembler.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/lifecycle.d.ts +8 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -2
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook-http.d.ts +39 -0
- package/dist/plugin/shell-hook-http.d.ts.map +1 -0
- package/dist/plugin/shell-hook-http.js +163 -0
- package/dist/plugin/shell-hook-http.js.map +1 -0
- package/dist/plugin/shell-hook.d.ts +93 -11
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +223 -39
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +2 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +1 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +34 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +2 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +25 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +6 -2
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +6 -4
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +2 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -1
- package/dist/runtime/query/path-escalation.js +7 -4
- package/dist/runtime/query/path-escalation.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +2 -2
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts +2 -0
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +25 -0
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +3 -2
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +32 -13
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/session/retention/archive.js +1 -1
- package/dist/session/retention/archive.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +6 -0
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +28 -0
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/loader.d.ts +7 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +53 -0
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/registry.d.ts +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +2 -1
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts +52 -0
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
- package/dist/tools/builtins/bash-cwd.js +127 -0
- package/dist/tools/builtins/bash-cwd.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +41 -6
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
- package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
- package/dist/tools/builtins/bounded-file-read.js +67 -0
- package/dist/tools/builtins/bounded-file-read.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts +6 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +187 -24
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +113 -28
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-media.d.ts +68 -0
- package/dist/tools/builtins/read-media.d.ts.map +1 -0
- package/dist/tools/builtins/read-media.js +397 -0
- package/dist/tools/builtins/read-media.js.map +1 -0
- package/dist/tools/builtins/search-tools.js +1 -1
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +4 -2
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +61 -2
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/builtins/sleep-guard.d.ts +15 -0
- package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
- package/dist/tools/builtins/sleep-guard.js +33 -0
- package/dist/tools/builtins/sleep-guard.js.map +1 -0
- package/dist/tools/builtins/view-image.d.ts +23 -1
- package/dist/tools/builtins/view-image.d.ts.map +1 -1
- package/dist/tools/builtins/view-image.js +66 -58
- package/dist/tools/builtins/view-image.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +31 -0
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/toolsets/manager.js +3 -3
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +6 -2
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/agent/task.js.map +1 -1
- package/dist/types/connector/mcp.d.ts +46 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +1 -1
- package/dist/types/plugin/index.d.ts +72 -5
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +12 -1
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/session/sub-session.d.ts +6 -2
- package/dist/types/session/sub-session.d.ts.map +1 -1
- package/dist/types/skills/index.d.ts +36 -0
- package/dist/types/skills/index.d.ts.map +1 -1
- package/dist/types/skills/index.js +9 -0
- package/dist/types/skills/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +22 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/workspace/ref.d.ts +2 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/frontmatter.d.ts +12 -0
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +6 -0
- package/dist/utils/frontmatter.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +2 -0
- package/src/agents/file-definitions.ts +126 -6
- package/src/authorization/protected-paths.ts +30 -2
- package/src/connector/mcp/client.ts +87 -1
- package/src/constants/mcp/index.ts +3 -0
- package/src/constants/plugin/index.ts +2 -0
- package/src/manager/agent/lifecycle.ts +24 -5
- package/src/manager/session/turn-recorder.ts +2 -1
- package/src/persona/assembler.ts +2 -2
- package/src/plugin/index.ts +5 -0
- package/src/plugin/lifecycle.ts +16 -2
- package/src/plugin/shell-hook-http.ts +181 -0
- package/src/plugin/shell-hook.ts +386 -89
- package/src/public-runtime.ts +2 -0
- package/src/public-tools.ts +2 -0
- package/src/public-types.ts +5 -0
- package/src/runtime/query/executor.ts +40 -10
- package/src/runtime/query/index.ts +33 -0
- package/src/runtime/query/iteration/index.ts +11 -3
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
- package/src/runtime/query/iteration/phases/compaction.ts +21 -4
- package/src/runtime/query/iteration/phases/context.ts +2 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
- package/src/runtime/query/path-escalation.ts +11 -5
- package/src/runtime/query/plugin-hooks.ts +2 -2
- package/src/runtime/query/prepare-turn.ts +33 -0
- package/src/runtime/query/review-policy.ts +45 -16
- package/src/session/retention/archive.ts +1 -1
- package/src/session/workspace/driver.ts +6 -0
- package/src/session/workspace/git-worktree.ts +31 -0
- package/src/skills/index.ts +6 -1
- package/src/skills/loader.ts +54 -0
- package/src/skills/registry.ts +7 -6
- package/src/tools/builtins/bash-cwd.ts +156 -0
- package/src/tools/builtins/bash.ts +79 -34
- package/src/tools/builtins/bounded-file-read.ts +80 -0
- package/src/tools/builtins/grep.ts +198 -28
- package/src/tools/builtins/read-file.ts +151 -30
- package/src/tools/builtins/read-media.ts +483 -0
- package/src/tools/builtins/search-tools.ts +1 -1
- package/src/tools/builtins/skill.ts +91 -2
- package/src/tools/builtins/sleep-guard.ts +31 -0
- package/src/tools/builtins/view-image.ts +74 -62
- package/src/tools/file-read-tracker.ts +30 -0
- package/src/toolsets/manager.ts +3 -3
- package/src/types/agent/query.ts +2 -0
- package/src/types/agent/task.ts +6 -2
- package/src/types/connector/mcp.ts +48 -0
- package/src/types/hitl/index.ts +1 -1
- package/src/types/plugin/index.ts +102 -16
- package/src/types/session/sub-session.ts +6 -2
- package/src/types/skills/index.ts +48 -0
- package/src/types/tool/index.ts +22 -0
- package/src/types/workspace/ref.ts +2 -0
- package/src/utils/frontmatter.ts +21 -0
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
MCPConnectionStatus,
|
|
7
7
|
MCPContentBlock,
|
|
8
8
|
MCPDiscoverResult,
|
|
9
|
+
MCPElicitationRequest,
|
|
10
|
+
MCPElicitationResult,
|
|
9
11
|
MCPEraCache,
|
|
10
12
|
MCPEventListener,
|
|
11
13
|
MCPInitializeResult,
|
|
@@ -58,6 +60,7 @@ import { type McpParamHeaderBinding, validateMcpHeaderAnnotations } from './x-mc
|
|
|
58
60
|
import {
|
|
59
61
|
DEFAULT_MCP_ERA_PROBE_TIMEOUT_MS,
|
|
60
62
|
DEFAULT_MCP_REQUEST_TIMEOUT_MS,
|
|
63
|
+
JSON_RPC_INVALID_PARAMS,
|
|
61
64
|
JSON_RPC_METHOD_NOT_FOUND,
|
|
62
65
|
MCP_DISCOVER_METHOD,
|
|
63
66
|
MCP_LEGACY_VERSIONS,
|
|
@@ -243,6 +246,8 @@ export class MCPClient {
|
|
|
243
246
|
private subscriptionFailures = 0
|
|
244
247
|
/** Best-effort protocol cancellations still belong to this connection generation. */
|
|
245
248
|
private cancellationControllers = new Set<AbortController>()
|
|
249
|
+
/** Elicitations waiting on the person, by the server's request id. */
|
|
250
|
+
private elicitationControllers = new Map<string | number, AbortController>()
|
|
246
251
|
private log: Logger
|
|
247
252
|
private readonly config: MCPClientConfig
|
|
248
253
|
private readonly requestTimeoutMs: number
|
|
@@ -383,7 +388,10 @@ export class MCPClient {
|
|
|
383
388
|
const offered = MCP_LEGACY_VERSIONS[0]
|
|
384
389
|
const result = (await this.request('initialize', {
|
|
385
390
|
protocolVersion: offered,
|
|
386
|
-
capabilities:
|
|
391
|
+
capabilities: {
|
|
392
|
+
...(this.config.capabilities ?? {}),
|
|
393
|
+
...(this.config.onElicitation ? { elicitation: {} } : {}),
|
|
394
|
+
},
|
|
387
395
|
clientInfo: this.config.clientInfo ?? NAMZU_CLIENT_INFO,
|
|
388
396
|
})) as MCPInitializeResult
|
|
389
397
|
|
|
@@ -1555,6 +1563,13 @@ export class MCPClient {
|
|
|
1555
1563
|
}
|
|
1556
1564
|
}
|
|
1557
1565
|
|
|
1566
|
+
if (message.method === 'notifications/cancelled' && message.id === undefined) {
|
|
1567
|
+
const requestId = message.params?.requestId
|
|
1568
|
+
if (typeof requestId === 'string' || typeof requestId === 'number') {
|
|
1569
|
+
this.elicitationControllers.get(requestId)?.abort(new Error('Withdrawn by the server'))
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1558
1573
|
if (message.method && message.id === undefined) {
|
|
1559
1574
|
for (const handler of this.notificationHandlers) {
|
|
1560
1575
|
handler(message.method, message.params)
|
|
@@ -1568,6 +1583,15 @@ export class MCPClient {
|
|
|
1568
1583
|
// dropped on the floor, so a spec-current server sat waiting for a
|
|
1569
1584
|
// reply that would never come, which looks exactly like a hang.
|
|
1570
1585
|
// Answer honestly: we do not implement these yet.
|
|
1586
|
+
if (
|
|
1587
|
+
message.method === 'elicitation/create' &&
|
|
1588
|
+
message.id !== undefined &&
|
|
1589
|
+
this.config.onElicitation &&
|
|
1590
|
+
this.era?.kind === 'legacy'
|
|
1591
|
+
) {
|
|
1592
|
+
void this.answerElicitation(message.id, message.params)
|
|
1593
|
+
return
|
|
1594
|
+
}
|
|
1571
1595
|
if (message.method && message.id !== undefined) {
|
|
1572
1596
|
this.log.warn('Declining unsupported server-initiated MCP request', {
|
|
1573
1597
|
'namzu.connector.server': this.config.serverName,
|
|
@@ -1590,6 +1614,68 @@ export class MCPClient {
|
|
|
1590
1614
|
}
|
|
1591
1615
|
}
|
|
1592
1616
|
|
|
1617
|
+
/**
|
|
1618
|
+
* Answer one `elicitation/create`: form mode goes to the host's handler,
|
|
1619
|
+
* anything else is refused as invalid params so the server is never left
|
|
1620
|
+
* waiting. Only the action is logged, never the values the person typed.
|
|
1621
|
+
*/
|
|
1622
|
+
private async answerElicitation(
|
|
1623
|
+
id: string | number,
|
|
1624
|
+
params: Record<string, unknown> | undefined,
|
|
1625
|
+
): Promise<void> {
|
|
1626
|
+
const handler = this.config.onElicitation
|
|
1627
|
+
const reply = (body: { result: unknown } | { error: { code: number; message: string } }) =>
|
|
1628
|
+
this.transport.send({ jsonrpc: '2.0', id, ...body }).catch((err) => {
|
|
1629
|
+
this.log.debug('Failed to send elicitation reply', {
|
|
1630
|
+
'exception.message': toErrorMessage(err),
|
|
1631
|
+
})
|
|
1632
|
+
})
|
|
1633
|
+
const schema = params?.requestedSchema
|
|
1634
|
+
const isForm = params?.mode === undefined || params.mode === 'form'
|
|
1635
|
+
if (
|
|
1636
|
+
!handler ||
|
|
1637
|
+
!isForm ||
|
|
1638
|
+
typeof params?.message !== 'string' ||
|
|
1639
|
+
typeof schema !== 'object' ||
|
|
1640
|
+
schema === null
|
|
1641
|
+
) {
|
|
1642
|
+
await reply({
|
|
1643
|
+
error: {
|
|
1644
|
+
code: JSON_RPC_INVALID_PARAMS,
|
|
1645
|
+
message: isForm ? 'Invalid elicitation request' : 'Only form elicitation is supported',
|
|
1646
|
+
},
|
|
1647
|
+
})
|
|
1648
|
+
return
|
|
1649
|
+
}
|
|
1650
|
+
const controller = new AbortController()
|
|
1651
|
+
this.elicitationControllers.set(id, controller)
|
|
1652
|
+
this.cancellationControllers.add(controller)
|
|
1653
|
+
let result: MCPElicitationResult
|
|
1654
|
+
try {
|
|
1655
|
+
result = await handler(
|
|
1656
|
+
{
|
|
1657
|
+
serverName: this.config.serverName,
|
|
1658
|
+
message: params.message,
|
|
1659
|
+
requestedSchema: schema as MCPElicitationRequest['requestedSchema'],
|
|
1660
|
+
},
|
|
1661
|
+
{ signal: controller.signal },
|
|
1662
|
+
)
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
this.log.warn('Elicitation handler failed', { 'exception.message': toErrorMessage(err) })
|
|
1665
|
+
result = { action: 'cancel' }
|
|
1666
|
+
} finally {
|
|
1667
|
+
this.elicitationControllers.delete(id)
|
|
1668
|
+
this.cancellationControllers.delete(controller)
|
|
1669
|
+
}
|
|
1670
|
+
// The server withdrew the request: it is no longer listening.
|
|
1671
|
+
if (controller.signal.aborted) return
|
|
1672
|
+
this.log.info('Elicitation answered', {
|
|
1673
|
+
'namzu.connector.server': this.config.serverName,
|
|
1674
|
+
'namzu.connector.elicitation.action': result.action,
|
|
1675
|
+
})
|
|
1676
|
+
await reply({ result: result.action === 'accept' ? result : { action: result.action } })
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1593
1679
|
private handleProgressNotification(params: Record<string, unknown> | undefined): void {
|
|
1594
1680
|
const token = params?.progressToken
|
|
1595
1681
|
if (typeof token !== 'string') return
|
|
@@ -70,6 +70,9 @@ export const MCP_SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = [
|
|
|
70
70
|
*/
|
|
71
71
|
export const DEFAULT_MCP_REQUEST_TIMEOUT_MS = 30_000
|
|
72
72
|
|
|
73
|
+
/** JSON-RPC 2.0 reserved code for parameters the receiver cannot accept. */
|
|
74
|
+
export const JSON_RPC_INVALID_PARAMS = -32602
|
|
75
|
+
|
|
73
76
|
/** JSON-RPC 2.0 reserved code for an unimplemented method. */
|
|
74
77
|
export const JSON_RPC_METHOD_NOT_FOUND = -32601
|
|
75
78
|
|
|
@@ -32,6 +32,8 @@ export const MAX_MCP_SERVERS_PER_PLUGIN = 5
|
|
|
32
32
|
export const MAX_SKILLS_PER_PLUGIN = 20
|
|
33
33
|
export const MAX_CONNECTORS_PER_PLUGIN = 10
|
|
34
34
|
export const MAX_PERSONAS_PER_PLUGIN = 5
|
|
35
|
+
export const MAX_COMMAND_DIRS_PER_PLUGIN = 10
|
|
36
|
+
export const MAX_AGENT_DIRS_PER_PLUGIN = 10
|
|
35
37
|
|
|
36
38
|
/** Hook execution */
|
|
37
39
|
export const HOOK_TIMEOUT_MS = 600_000
|
|
@@ -170,7 +170,7 @@ interface ChildSpawnRecord {
|
|
|
170
170
|
/** Removes the child's log from the process-local lookup. */
|
|
171
171
|
releaseLog?: () => void
|
|
172
172
|
workspaceRef?: WorkspaceRef
|
|
173
|
-
workspaceRetention?: 'retain'
|
|
173
|
+
workspaceRetention?: 'retain' | 'retain-changed'
|
|
174
174
|
/**
|
|
175
175
|
* What this child was actually granted, after the ancestor union.
|
|
176
176
|
*
|
|
@@ -1739,8 +1739,9 @@ export class AgentManager {
|
|
|
1739
1739
|
subSession = {
|
|
1740
1740
|
...subSession,
|
|
1741
1741
|
workspaceId: workspaceRef.id,
|
|
1742
|
-
...(request?.mode === 'isolated' &&
|
|
1743
|
-
|
|
1742
|
+
...(request?.mode === 'isolated' &&
|
|
1743
|
+
(request.retention === 'retain' || request.retention === 'retain-changed')
|
|
1744
|
+
? { workspaceRetention: request.retention }
|
|
1744
1745
|
: {}),
|
|
1745
1746
|
}
|
|
1746
1747
|
await store.updateSubSession(subSession, context.tenantId)
|
|
@@ -1781,8 +1782,9 @@ export class AgentManager {
|
|
|
1781
1782
|
childDepth,
|
|
1782
1783
|
childOwnerVersion: childSession.ownerVersion,
|
|
1783
1784
|
workspaceRef,
|
|
1784
|
-
...(options.workspace?.mode === 'isolated' &&
|
|
1785
|
-
|
|
1785
|
+
...(options.workspace?.mode === 'isolated' &&
|
|
1786
|
+
(options.workspace.retention === 'retain' || options.workspace.retention === 'retain-changed')
|
|
1787
|
+
? { workspaceRetention: options.workspace.retention }
|
|
1786
1788
|
: {}),
|
|
1787
1789
|
}
|
|
1788
1790
|
}
|
|
@@ -2394,6 +2396,23 @@ export class AgentManager {
|
|
|
2394
2396
|
if (!spawnRecord.workspaceRef) return
|
|
2395
2397
|
const backend = spawnRecord.workspaceRef.meta.backend
|
|
2396
2398
|
if (!this.deps.workspaceRegistry.has(backend)) return
|
|
2399
|
+
if (spawnRecord.workspaceRetention === 'retain-changed') {
|
|
2400
|
+
// Keep the checkout unless it provably holds nothing. A look that fails,
|
|
2401
|
+
// or a backend that cannot say whether it moved, keeps it too.
|
|
2402
|
+
const seen = await this.deps.workspaceRegistry
|
|
2403
|
+
.get(backend)
|
|
2404
|
+
.inspect(spawnRecord.workspaceRef)
|
|
2405
|
+
.catch((inspectErr) => {
|
|
2406
|
+
this.log.warn('Workspace inspect failed; keeping it', {
|
|
2407
|
+
'namzu.manager.backend': backend,
|
|
2408
|
+
'namzu.sub_session.id': spawnRecord.subSessionId,
|
|
2409
|
+
'exception.message': toErrorMessage(inspectErr),
|
|
2410
|
+
})
|
|
2411
|
+
return undefined
|
|
2412
|
+
})
|
|
2413
|
+
if (!seen) return
|
|
2414
|
+
if (seen.exists && (seen.isDirty || seen.hasNewCommits !== false)) return
|
|
2415
|
+
}
|
|
2397
2416
|
await this.deps.workspaceRegistry
|
|
2398
2417
|
.get(backend)
|
|
2399
2418
|
.dispose(spawnRecord.workspaceRef)
|
|
@@ -228,7 +228,8 @@ export class TurnRecorder {
|
|
|
228
228
|
#chain: Promise<void> = Promise.resolve()
|
|
229
229
|
#failure: unknown = undefined
|
|
230
230
|
#view: ViewEntry[] = []
|
|
231
|
-
|
|
231
|
+
// Identity bookkeeping must not keep messages removed by compaction alive.
|
|
232
|
+
readonly #ids = new WeakMap<Message, MessageId>()
|
|
232
233
|
/** Every message's record id, kept after a compaction drops it from the fold. */
|
|
233
234
|
readonly #recorded = new WeakMap<Message, MessageId>()
|
|
234
235
|
readonly #transient = new WeakSet<Message>()
|
package/src/persona/assembler.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentPersona, OutputDiscipline } from '../types/persona/index.js'
|
|
2
|
-
import { type Skill, isInvocableBy } from '../types/skills/index.js'
|
|
2
|
+
import { type Skill, isInvocableBy, skillModelDescription } from '../types/skills/index.js'
|
|
3
3
|
|
|
4
4
|
export function assembleSystemPrompt(persona: AgentPersona, skills?: Skill[]): string {
|
|
5
5
|
const sections: string[] = []
|
|
@@ -58,7 +58,7 @@ export function renderSkillsSection(skills?: Skill[]): string | null {
|
|
|
58
58
|
const lines = [
|
|
59
59
|
'<skill>',
|
|
60
60
|
`<name>${s.metadata.name}</name>`,
|
|
61
|
-
`<description>${s.metadata
|
|
61
|
+
`<description>${skillModelDescription(s.metadata)}</description>`,
|
|
62
62
|
`<location>${s.dirPath}/SKILL.md</location>`,
|
|
63
63
|
]
|
|
64
64
|
if (s.metadata.compatibility) {
|
package/src/plugin/index.ts
CHANGED
|
@@ -17,8 +17,13 @@ export {
|
|
|
17
17
|
shellHookVerdict,
|
|
18
18
|
} from './shell-hook.js'
|
|
19
19
|
export type {
|
|
20
|
+
HttpHookEntry,
|
|
21
|
+
PromptHookEntry,
|
|
20
22
|
ShellHookEntry,
|
|
21
23
|
ShellHookEvent,
|
|
24
|
+
ShellHookHandler,
|
|
25
|
+
ShellHookJudge,
|
|
26
|
+
ShellHookOnFailure,
|
|
22
27
|
ShellHookInput,
|
|
23
28
|
ShellHookOptions,
|
|
24
29
|
ShellHookOutcome,
|
package/src/plugin/lifecycle.ts
CHANGED
|
@@ -180,6 +180,8 @@ function immutableManifest(manifest: PluginDefinition['manifest']): PluginDefini
|
|
|
180
180
|
...(mcpServers ? { mcpServers: Object.freeze(mcpServers) } : {}),
|
|
181
181
|
...(manifest.connectors ? { connectors: Object.freeze([...manifest.connectors]) } : {}),
|
|
182
182
|
...(manifest.personas ? { personas: Object.freeze([...manifest.personas]) } : {}),
|
|
183
|
+
...(manifest.commands ? { commands: Object.freeze([...manifest.commands]) } : {}),
|
|
184
|
+
...(manifest.agents ? { agents: Object.freeze([...manifest.agents]) } : {}),
|
|
183
185
|
})
|
|
184
186
|
}
|
|
185
187
|
|
|
@@ -438,11 +440,23 @@ export class PluginLifecycleManager {
|
|
|
438
440
|
if (index >= 0) this.listeners.splice(index, 1)
|
|
439
441
|
}
|
|
440
442
|
|
|
441
|
-
|
|
443
|
+
/**
|
|
444
|
+
* `options.authorityRoot` names the folder the plugin must stay inside when
|
|
445
|
+
* it is not the scope's own root: a plugin a person points at directly for
|
|
446
|
+
* one session is its own authority. It applies to this admission only.
|
|
447
|
+
*/
|
|
448
|
+
async install(
|
|
449
|
+
pluginDir: string,
|
|
450
|
+
scope: PluginScope,
|
|
451
|
+
options: { readonly authorityRoot?: string } = {},
|
|
452
|
+
): Promise<PluginDefinition> {
|
|
442
453
|
// Admission and enable read the same constructor-owned capability. The
|
|
443
454
|
// loader defaults this to false for direct callers; a manager that really
|
|
444
455
|
// owns the registry is the authority that can opt the manifest in.
|
|
445
|
-
const rootDir = await resolveWithinReal(
|
|
456
|
+
const rootDir = await resolveWithinReal(
|
|
457
|
+
options.authorityRoot ?? this.scopeRoots[scope],
|
|
458
|
+
pluginDir,
|
|
459
|
+
)
|
|
446
460
|
const manifest = immutableManifest(
|
|
447
461
|
await loadPluginManifest(rootDir, {
|
|
448
462
|
skillsSupported: Boolean(this.skillRegistry),
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An HTTP hook: the event as a JSON POST, the response as the hook's answer.
|
|
3
|
+
*
|
|
4
|
+
* A 2xx response is exit `0` with the body as stdout, so the JSON answer is
|
|
5
|
+
* read exactly as a command's is. Any other status, a network error and a
|
|
6
|
+
* deadline are the hook's own failure. The address is checked where the
|
|
7
|
+
* connection is made, not before it: a name that resolves to a private or
|
|
8
|
+
* link-local address (a cloud metadata service, a router) is refused, so a
|
|
9
|
+
* hook cannot be pointed at the machine's neighbours and a name cannot be
|
|
10
|
+
* re-resolved between the check and the connect. Loopback stays allowed for
|
|
11
|
+
* local development. Redirects are not followed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { type LookupAddress, lookup } from 'node:dns'
|
|
15
|
+
import { request as httpRequest } from 'node:http'
|
|
16
|
+
import { request as httpsRequest } from 'node:https'
|
|
17
|
+
import { isIP } from 'node:net'
|
|
18
|
+
|
|
19
|
+
/** Captured response bound. */
|
|
20
|
+
const MAX_BODY_BYTES = 64 * 1024
|
|
21
|
+
|
|
22
|
+
export interface HttpHookRequest {
|
|
23
|
+
readonly url: string
|
|
24
|
+
readonly body: string
|
|
25
|
+
readonly headers?: Readonly<Record<string, string>> | undefined
|
|
26
|
+
readonly allowedEnvVars?: readonly string[] | undefined
|
|
27
|
+
readonly timeoutMs: number
|
|
28
|
+
readonly env?: Readonly<Record<string, string | undefined>> | undefined
|
|
29
|
+
readonly signal?: AbortSignal | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface HttpHookResponse {
|
|
33
|
+
readonly status: number | null
|
|
34
|
+
readonly body: string
|
|
35
|
+
readonly timedOut: boolean
|
|
36
|
+
readonly error?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Replace `$NAME` and `${NAME}` in a header value with the environment's
|
|
41
|
+
* value, but only for names the hook listed. Any other reference becomes the
|
|
42
|
+
* empty string, so a header can never carry a variable its author did not
|
|
43
|
+
* name. Line breaks and NUL are removed from the result, so a value cannot add
|
|
44
|
+
* a header of its own.
|
|
45
|
+
*/
|
|
46
|
+
export function interpolateHookHeader(
|
|
47
|
+
value: string,
|
|
48
|
+
allowed: ReadonlySet<string>,
|
|
49
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
50
|
+
): string {
|
|
51
|
+
return value
|
|
52
|
+
.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_all, braced, bare) => {
|
|
53
|
+
const name = (braced ?? bare) as string
|
|
54
|
+
return allowed.has(name) ? (env[name] ?? '') : ''
|
|
55
|
+
})
|
|
56
|
+
.replace(/[\r\n\0]/g, '')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether an address may be connected to: public, or loopback. */
|
|
60
|
+
export function isReachableHookAddress(address: string): boolean {
|
|
61
|
+
const family = isIP(address)
|
|
62
|
+
if (family === 4) {
|
|
63
|
+
const [a = 0, b = 0] = address.split('.').map(Number)
|
|
64
|
+
if (a === 127) return true
|
|
65
|
+
if (a === 0 || a === 10) return false
|
|
66
|
+
if (a === 169 && b === 254) return false
|
|
67
|
+
if (a === 172 && b >= 16 && b <= 31) return false
|
|
68
|
+
if (a === 192 && b === 168) return false
|
|
69
|
+
if (a === 100 && b >= 64 && b <= 127) return false
|
|
70
|
+
if (a >= 224) return false
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
if (family === 6) {
|
|
74
|
+
const lower = address.toLowerCase()
|
|
75
|
+
if (lower === '::1') return true
|
|
76
|
+
if (lower === '::') return false
|
|
77
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower)
|
|
78
|
+
if (mapped?.[1]) return isReachableHookAddress(mapped[1])
|
|
79
|
+
if (/^f[cd]/.test(lower) || /^fe[89ab]/.test(lower)) return false
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `dns.lookup` that refuses an address a hook may not reach. */
|
|
86
|
+
function guardedLookup(
|
|
87
|
+
hostname: string,
|
|
88
|
+
options: { all?: boolean; family?: number | 'IPv4' | 'IPv6' },
|
|
89
|
+
callback: (error: Error | null, address?: string | LookupAddress[], family?: number) => void,
|
|
90
|
+
): void {
|
|
91
|
+
lookup(hostname, { ...options, all: true }, (error: Error | null, addresses: LookupAddress[]) => {
|
|
92
|
+
if (error) {
|
|
93
|
+
callback(error)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
const list = addresses
|
|
97
|
+
const refused = list.find((entry) => !isReachableHookAddress(entry.address))
|
|
98
|
+
if (refused || list.length === 0) {
|
|
99
|
+
callback(
|
|
100
|
+
new Error(
|
|
101
|
+
`${hostname} resolves to ${refused?.address ?? 'nothing'}, a private or link-local address`,
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
if (options.all) {
|
|
107
|
+
callback(null, list)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
const first = list[0] as LookupAddress
|
|
111
|
+
callback(null, first.address, first.family)
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function postHookJson(input: HttpHookRequest): Promise<HttpHookResponse> {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
let url: URL
|
|
118
|
+
try {
|
|
119
|
+
url = new URL(input.url)
|
|
120
|
+
} catch {
|
|
121
|
+
resolve({ status: null, body: '', timedOut: false, error: 'the url is not valid' })
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
125
|
+
resolve({ status: null, body: '', timedOut: false, error: 'the url must be http or https' })
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
// A literal address never reaches the resolver, so it is checked here.
|
|
129
|
+
const literal = url.hostname.replace(/^\[|\]$/g, '')
|
|
130
|
+
if (isIP(literal) !== 0 && !isReachableHookAddress(literal)) {
|
|
131
|
+
resolve({
|
|
132
|
+
status: null,
|
|
133
|
+
body: '',
|
|
134
|
+
timedOut: false,
|
|
135
|
+
error: `${literal} is a private or link-local address`,
|
|
136
|
+
})
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
const allowed = new Set(input.allowedEnvVars ?? [])
|
|
140
|
+
const env = input.env ?? process.env
|
|
141
|
+
const headers: Record<string, string> = {
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
'Content-Length': String(Buffer.byteLength(input.body)),
|
|
144
|
+
}
|
|
145
|
+
for (const [name, value] of Object.entries(input.headers ?? {})) {
|
|
146
|
+
headers[name] = interpolateHookHeader(value, allowed, env)
|
|
147
|
+
}
|
|
148
|
+
let settled = false
|
|
149
|
+
let timedOut = false
|
|
150
|
+
const finish = (response: HttpHookResponse) => {
|
|
151
|
+
if (settled) return
|
|
152
|
+
settled = true
|
|
153
|
+
clearTimeout(timer)
|
|
154
|
+
input.signal?.removeEventListener('abort', onAbort)
|
|
155
|
+
resolve(response)
|
|
156
|
+
}
|
|
157
|
+
const onAbort = () => {
|
|
158
|
+
req.destroy()
|
|
159
|
+
finish({ status: null, body: '', timedOut: false, error: 'cancelled' })
|
|
160
|
+
}
|
|
161
|
+
const send = url.protocol === 'https:' ? httpsRequest : httpRequest
|
|
162
|
+
const req = send(url, { method: 'POST', headers, lookup: guardedLookup as never }, (res) => {
|
|
163
|
+
let body = ''
|
|
164
|
+
res.setEncoding('utf8')
|
|
165
|
+
res.on('data', (chunk: string) => {
|
|
166
|
+
if (body.length < MAX_BODY_BYTES) body = (body + chunk).slice(0, MAX_BODY_BYTES)
|
|
167
|
+
})
|
|
168
|
+
res.on('end', () => finish({ status: res.statusCode ?? null, body, timedOut: false }))
|
|
169
|
+
res.on('error', (error) => finish({ status: null, body, timedOut, error: error.message }))
|
|
170
|
+
})
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
timedOut = true
|
|
173
|
+
req.destroy()
|
|
174
|
+
finish({ status: null, body: '', timedOut: true })
|
|
175
|
+
}, input.timeoutMs)
|
|
176
|
+
req.on('error', (error) => finish({ status: null, body: '', timedOut, error: error.message }))
|
|
177
|
+
if (input.signal?.aborted) onAbort()
|
|
178
|
+
else input.signal?.addEventListener('abort', onAbort, { once: true })
|
|
179
|
+
req.end(input.body)
|
|
180
|
+
})
|
|
181
|
+
}
|