@namzu/sdk 60.1.0 → 60.2.1
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 +28 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +3 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +16 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/contracts/session/api.d.ts +1 -1
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +7 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +19 -0
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/peers/dir.d.ts.map +1 -1
- package/dist/peers/dir.js +3 -0
- package/dist/peers/dir.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +17 -0
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +23 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +88 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +13 -0
- 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 +3 -2
- package/dist/runtime/query/iteration/index.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 +6 -0
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/request-void-turns.d.ts +19 -0
- package/dist/runtime/query/request-void-turns.d.ts.map +1 -0
- package/dist/runtime/query/request-void-turns.js +26 -0
- package/dist/runtime/query/request-void-turns.js.map +1 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +12 -2
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +6 -5
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/session/paths.d.ts +15 -22
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +19 -46
- package/dist/session/paths.js.map +1 -1
- package/dist/store/session-log/core.d.ts +43 -0
- package/dist/store/session-log/core.d.ts.map +1 -1
- package/dist/store/session-log/core.js +190 -3
- package/dist/store/session-log/core.js.map +1 -1
- package/dist/store/session-log/disk.d.ts +1 -1
- package/dist/store/session-log/disk.d.ts.map +1 -1
- package/dist/store/session-log/disk.js +1 -0
- package/dist/store/session-log/disk.js.map +1 -1
- package/dist/store/session-log/index.d.ts +1 -1
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -1
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -1
- package/dist/tools/builtins/bash-cwd.js +2 -3
- package/dist/tools/builtins/bash-cwd.js.map +1 -1
- package/dist/tools/builtins/read-media.js +3 -3
- package/dist/tools/builtins/read-media.js.map +1 -1
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +2 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/hitl/index.d.ts +8 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/session/events.d.ts +38 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js +4 -0
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +10 -10
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +2 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/utils/temp.d.ts +82 -0
- package/dist/utils/temp.d.ts.map +1 -0
- package/dist/utils/temp.js +196 -0
- package/dist/utils/temp.js.map +1 -0
- package/package.json +1 -1
- package/src/bridge/a2a/mapper.ts +3 -0
- package/src/bridge/sse/mapper.ts +16 -0
- package/src/contracts/session/api.ts +2 -0
- package/src/manager/session/turn-recorder.ts +21 -0
- package/src/peers/dir.ts +3 -0
- package/src/public-runtime.ts +13 -0
- package/src/runtime/query/events.ts +19 -0
- package/src/runtime/query/executor.ts +92 -8
- package/src/runtime/query/index.ts +12 -0
- package/src/runtime/query/iteration/index.ts +3 -2
- package/src/runtime/query/iteration/phases/tool-review.ts +6 -0
- package/src/runtime/query/request-void-turns.ts +28 -0
- package/src/runtime/query/review-policy.ts +13 -2
- package/src/runtime/query/tooling.ts +3 -0
- package/src/sandbox/provider/local.ts +8 -5
- package/src/session/paths.ts +32 -57
- package/src/store/session-log/core.ts +235 -3
- package/src/store/session-log/disk.ts +7 -1
- package/src/store/session-log/index.ts +4 -0
- package/src/tools/builtins/bash-cwd.ts +2 -3
- package/src/tools/builtins/read-media.ts +3 -3
- package/src/turn/reporter.ts +2 -0
- package/src/types/hitl/index.ts +8 -0
- package/src/types/session/events.ts +41 -1
- package/src/types/session/records.ts +2 -0
- package/src/utils/temp.ts +250 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { join, resolve } from 'node:path'
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path'
|
|
3
3
|
import type { Span } from '@opentelemetry/api'
|
|
4
4
|
import type { AuthorizationGate } from '../../authorization/gate.js'
|
|
5
5
|
import { unknownProgramInLine } from '../../authorization/program.js'
|
|
@@ -24,7 +24,7 @@ import { fingerprintContent } from '../../tools/builtins/content-fingerprint.js'
|
|
|
24
24
|
import { sandboxShellSpawn, withoutBashStartup } from '../../tools/command-shell.js'
|
|
25
25
|
import { isAlwaysDestructive } from '../../tools/defineTool.js'
|
|
26
26
|
import { createFileReadTracker } from '../../tools/file-read-tracker.js'
|
|
27
|
-
import { pathOutsideRoots, toolRoots } from '../../tools/paths.js'
|
|
27
|
+
import { pathOutsideRoots, resolveWithinAnyReal, toolRoots } from '../../tools/paths.js'
|
|
28
28
|
import { resolveRetainedOutput } from '../../tools/retained-output.js'
|
|
29
29
|
import type { ToolManager } from '../../toolsets/manager.js'
|
|
30
30
|
import type { ToolSourceRef } from '../../toolsets/types.js'
|
|
@@ -70,6 +70,7 @@ import { generateToolCallId } from '../../utils/id.js'
|
|
|
70
70
|
import type { Logger } from '../../utils/logger.js'
|
|
71
71
|
import { compressShellOutput } from '../../utils/shell-compress.js'
|
|
72
72
|
import { parseStructuredResultJson } from '../../utils/structured-result-json.js'
|
|
73
|
+
import { ensureScratchpadDirectory } from '../../utils/temp.js'
|
|
73
74
|
import { type BackgroundJobRegistry, type JobProcess, bindOwner } from '../jobs/registry.js'
|
|
74
75
|
import { type BatchDetach, stillRunningResult } from './detached-calls.js'
|
|
75
76
|
import {
|
|
@@ -155,12 +156,15 @@ export interface PreparedToolBatch {
|
|
|
155
156
|
readonly escalation?: ToolCallEscalation
|
|
156
157
|
/** What a `pre_tool_use` hook asked for this call's approval; see `ToolCallSummary.hookPermission`. */
|
|
157
158
|
readonly hookPermission?: PluginHookPermission
|
|
159
|
+
/** The call writes inside the session's scratch folder; see `ToolCallSummary.scratchpadWrite`. */
|
|
160
|
+
readonly scratchpadWrite?: true
|
|
158
161
|
}[]
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
interface OwnedPreparedToolBatch extends PreparedToolBatch {
|
|
162
165
|
readonly calls: ReadonlyMap<string, PreparedDirectCall>
|
|
163
166
|
readonly escalations: ReadonlyMap<string, ToolCallEscalation>
|
|
167
|
+
readonly scratchpadWrites: ReadonlySet<string>
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
/**
|
|
@@ -363,6 +367,13 @@ export interface ToolExecutorConfig {
|
|
|
363
367
|
workingDirectory: string
|
|
364
368
|
/** See `ToolContext.additionalDirectories`. */
|
|
365
369
|
additionalDirectories?: readonly string[]
|
|
370
|
+
/**
|
|
371
|
+
* Where this session's own scratch folder is (it need not exist). It becomes
|
|
372
|
+
* a root of the file tools, and a write inside it needs no review, once a call
|
|
373
|
+
* names it and the folder has been made and checked. Unset on a sandboxed
|
|
374
|
+
* turn and on a session with no layout on disk.
|
|
375
|
+
*/
|
|
376
|
+
scratchpadDirectory?: string
|
|
366
377
|
/** See `QueryParams.outsideRootAccess`. Default `'refuse'`. */
|
|
367
378
|
outsideRootAccess?: 'refuse' | 'review'
|
|
368
379
|
/** See `QueryParams.sandboxEscape`. Default `'refuse'`. */
|
|
@@ -814,6 +825,68 @@ export class ToolExecutor {
|
|
|
814
825
|
}
|
|
815
826
|
|
|
816
827
|
/** True once the turn has a sandbox, or is certain to get one before a tool runs. */
|
|
828
|
+
/** The scratch folder, once a call has named it and it has been made and checked. */
|
|
829
|
+
private scratchpadReady: string | undefined
|
|
830
|
+
|
|
831
|
+
/** The tools' roots now: the turn's, plus the scratch folder once it is ready. */
|
|
832
|
+
private rootsNow(): readonly string[] {
|
|
833
|
+
return toolRoots({
|
|
834
|
+
workingDirectory: this.config.workingDirectory,
|
|
835
|
+
additionalDirectories: this.additionalDirectoriesNow(),
|
|
836
|
+
})
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private additionalDirectoriesNow(): readonly string[] | undefined {
|
|
840
|
+
if (this.scratchpadReady === undefined) return this.config.additionalDirectories
|
|
841
|
+
return [...(this.config.additionalDirectories ?? []), this.scratchpadReady]
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Make the session's scratch folder a root when a call names it. Lazy: a
|
|
846
|
+
* session that never uses it leaves nothing on disk. The folder must be
|
|
847
|
+
* inside the private temporary root with no link on the way; otherwise it
|
|
848
|
+
* stays outside the roots and a path in it is asked about like any other.
|
|
849
|
+
*/
|
|
850
|
+
private admitScratchpad(call: PreparedSafetyCall): void {
|
|
851
|
+
const directory = this.config.scratchpadDirectory
|
|
852
|
+
if (directory === undefined || this.scratchpadReady !== undefined || this.sandboxed) return
|
|
853
|
+
if (call.kind === 'synthetic') return
|
|
854
|
+
let named = false
|
|
855
|
+
try {
|
|
856
|
+
named = JSON.stringify(call.input)?.includes(JSON.stringify(directory).slice(1, -1)) === true
|
|
857
|
+
} catch {
|
|
858
|
+
// An input that cannot be serialised names nothing.
|
|
859
|
+
}
|
|
860
|
+
if (!named) return
|
|
861
|
+
try {
|
|
862
|
+
this.scratchpadReady = ensureScratchpadDirectory(directory)
|
|
863
|
+
} catch (error) {
|
|
864
|
+
this.log.warn('The session scratch folder could not be made, so it stays outside the roots', {
|
|
865
|
+
'exception.message': error instanceof Error ? error.message : String(error),
|
|
866
|
+
})
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Whether `call` writes a file inside the ready scratch folder and nothing else about it is notable. */
|
|
871
|
+
private async writesScratchpad(
|
|
872
|
+
call: PreparedSafetyCall,
|
|
873
|
+
escalation: ToolCallEscalation | undefined,
|
|
874
|
+
): Promise<boolean> {
|
|
875
|
+
const root = this.scratchpadReady
|
|
876
|
+
if (root === undefined || escalation !== undefined || call.kind === 'synthetic') return false
|
|
877
|
+
if (call.toolName !== 'write' && call.toolName !== 'edit') return false
|
|
878
|
+
const tool = this.config.tools.get(call.toolName)
|
|
879
|
+
if (!tool?.pathArgument || call.input === null || typeof call.input !== 'object') return false
|
|
880
|
+
const value = (call.input as Record<string, unknown>)[tool.pathArgument]
|
|
881
|
+
if (typeof value !== 'string' || !isAbsolute(value)) return false
|
|
882
|
+
try {
|
|
883
|
+
await resolveWithinAnyReal([root], value)
|
|
884
|
+
return true
|
|
885
|
+
} catch {
|
|
886
|
+
return false
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
817
890
|
private get sandboxed(): boolean {
|
|
818
891
|
return this.config.sandbox !== undefined || this.sandboxGate !== undefined
|
|
819
892
|
}
|
|
@@ -1126,6 +1199,7 @@ export class ToolExecutor {
|
|
|
1126
1199
|
if ((response.message.toolCalls ?? []).length > 0) await this.awaitSandbox()
|
|
1127
1200
|
const calls = new Map<string, PreparedDirectCall>()
|
|
1128
1201
|
const escalations = new Map<string, ToolCallEscalation>()
|
|
1202
|
+
const scratchpadWrites = new Set<string>()
|
|
1129
1203
|
const earlierPaths: string[] = []
|
|
1130
1204
|
for (const toolCall of response.message.toolCalls ?? []) {
|
|
1131
1205
|
const prepared = await prepareDirectCall(
|
|
@@ -1137,11 +1211,13 @@ export class ToolExecutor {
|
|
|
1137
1211
|
const path = inputPathOf(prepared)
|
|
1138
1212
|
if (path !== undefined) earlierPaths.push(path)
|
|
1139
1213
|
calls.set(toolCall.id, call)
|
|
1214
|
+
this.admitScratchpad(call)
|
|
1140
1215
|
const escalation = await this.escalationOrUnfinished(call)
|
|
1141
1216
|
if (escalation) escalations.set(toolCall.id, escalation)
|
|
1217
|
+
if (await this.writesScratchpad(call, escalation)) scratchpadWrites.add(toolCall.id)
|
|
1142
1218
|
}
|
|
1143
1219
|
this.config.abortSignal.throwIfAborted()
|
|
1144
|
-
return this.publishPreparedBatch(calls, escalations)
|
|
1220
|
+
return this.publishPreparedBatch(calls, escalations, scratchpadWrites)
|
|
1145
1221
|
}
|
|
1146
1222
|
|
|
1147
1223
|
/**
|
|
@@ -1250,7 +1326,7 @@ export class ToolExecutor {
|
|
|
1250
1326
|
// result that was cut told the model to look.
|
|
1251
1327
|
const retained =
|
|
1252
1328
|
tool.name === 'read' && (await resolveRetainedOutput(this.outputDirectory(), value))
|
|
1253
|
-
const outside = retained ? undefined : await pathOutsideRoots(
|
|
1329
|
+
const outside = retained ? undefined : await pathOutsideRoots(this.rootsNow(), value)
|
|
1254
1330
|
if (outside !== undefined) outsidePaths = [outside]
|
|
1255
1331
|
}
|
|
1256
1332
|
}
|
|
@@ -1307,7 +1383,7 @@ export class ToolExecutor {
|
|
|
1307
1383
|
protectedPaths: readonly string[]
|
|
1308
1384
|
dangerousRemoval?: string
|
|
1309
1385
|
}> {
|
|
1310
|
-
const roots =
|
|
1386
|
+
const roots = this.rootsNow()
|
|
1311
1387
|
const protectedPaths: string[] = []
|
|
1312
1388
|
let outsidePaths: readonly string[] = []
|
|
1313
1389
|
let removal: string | undefined
|
|
@@ -1376,6 +1452,7 @@ export class ToolExecutor {
|
|
|
1376
1452
|
}
|
|
1377
1453
|
const calls = new Map((previous as OwnedPreparedToolBatch).calls)
|
|
1378
1454
|
const escalations = new Map((previous as OwnedPreparedToolBatch).escalations)
|
|
1455
|
+
const scratchpadWrites = new Set((previous as OwnedPreparedToolBatch).scratchpadWrites)
|
|
1379
1456
|
const earlierPaths: string[] = []
|
|
1380
1457
|
for (const toolCall of response.message.toolCalls ?? []) {
|
|
1381
1458
|
if (changedCallIds.has(toolCall.id)) {
|
|
@@ -1384,21 +1461,25 @@ export class ToolExecutor {
|
|
|
1384
1461
|
earlierPaths,
|
|
1385
1462
|
)
|
|
1386
1463
|
calls.set(toolCall.id, call)
|
|
1464
|
+
this.admitScratchpad(call)
|
|
1387
1465
|
const escalation = await this.escalationOrUnfinished(call)
|
|
1388
1466
|
if (escalation) escalations.set(toolCall.id, escalation)
|
|
1389
1467
|
else escalations.delete(toolCall.id)
|
|
1468
|
+
if (await this.writesScratchpad(call, escalation)) scratchpadWrites.add(toolCall.id)
|
|
1469
|
+
else scratchpadWrites.delete(toolCall.id)
|
|
1390
1470
|
}
|
|
1391
1471
|
const prepared = calls.get(toolCall.id)
|
|
1392
1472
|
const path = prepared ? inputPathOf(prepared) : undefined
|
|
1393
1473
|
if (path !== undefined) earlierPaths.push(path)
|
|
1394
1474
|
}
|
|
1395
1475
|
this.config.abortSignal.throwIfAborted()
|
|
1396
|
-
return this.publishPreparedBatch(calls, escalations)
|
|
1476
|
+
return this.publishPreparedBatch(calls, escalations, scratchpadWrites)
|
|
1397
1477
|
}
|
|
1398
1478
|
|
|
1399
1479
|
private publishPreparedBatch(
|
|
1400
1480
|
calls: ReadonlyMap<string, PreparedDirectCall>,
|
|
1401
1481
|
escalations: ReadonlyMap<string, ToolCallEscalation>,
|
|
1482
|
+
scratchpadWrites: ReadonlySet<string>,
|
|
1402
1483
|
): PreparedToolBatch {
|
|
1403
1484
|
const reviewCalls = [...calls.values()]
|
|
1404
1485
|
.filter(
|
|
@@ -1414,12 +1495,14 @@ export class ToolExecutor {
|
|
|
1414
1495
|
input: call.input,
|
|
1415
1496
|
...(escalation ? { escalation } : {}),
|
|
1416
1497
|
...(hookPermission ? { hookPermission } : {}),
|
|
1498
|
+
...(scratchpadWrites.has(call.toolCall.id) ? { scratchpadWrite: true as const } : {}),
|
|
1417
1499
|
}
|
|
1418
1500
|
})
|
|
1419
1501
|
const batch: OwnedPreparedToolBatch = Object.freeze({
|
|
1420
1502
|
reviewCalls: Object.freeze(reviewCalls),
|
|
1421
1503
|
calls: new Map(calls),
|
|
1422
1504
|
escalations: new Map(escalations),
|
|
1505
|
+
scratchpadWrites: new Set(scratchpadWrites),
|
|
1423
1506
|
})
|
|
1424
1507
|
this.preparedBatches.add(batch)
|
|
1425
1508
|
return batch
|
|
@@ -2032,6 +2115,7 @@ export class ToolExecutor {
|
|
|
2032
2115
|
} catch {
|
|
2033
2116
|
needsApproval = true
|
|
2034
2117
|
}
|
|
2118
|
+
this.admitScratchpad(preparedCall)
|
|
2035
2119
|
const nestedEscalation = await this.escalationOrUnfinished(preparedCall)
|
|
2036
2120
|
if (nestedEscalation || needsApproval || (gateResult && gateResult.decision !== 'allow')) {
|
|
2037
2121
|
const reason = nestedEscalation
|
|
@@ -2276,8 +2360,8 @@ export class ToolExecutor {
|
|
|
2276
2360
|
sessionId: this.config.sessionId,
|
|
2277
2361
|
turnId: this.config.turnId,
|
|
2278
2362
|
workingDirectory: this.config.workingDirectory,
|
|
2279
|
-
...(this.
|
|
2280
|
-
? { additionalDirectories: this.
|
|
2363
|
+
...(this.additionalDirectoriesNow()?.length
|
|
2364
|
+
? { additionalDirectories: this.additionalDirectoriesNow() }
|
|
2281
2365
|
: {}),
|
|
2282
2366
|
...(this.config.authorizationGate
|
|
2283
2367
|
? {
|
|
@@ -1542,6 +1542,17 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1542
1542
|
// pre-approves ends with the turn. Shared by the executor, where the
|
|
1543
1543
|
// `skill` tool records a grant, and the review phase, which reads it.
|
|
1544
1544
|
const skillGrants = new SkillGrantSet()
|
|
1545
|
+
// The session's own scratch folder, a root of the file tools once a call
|
|
1546
|
+
// names it. Not for a sandboxed turn (it is not mounted there), and not
|
|
1547
|
+
// for a session whose state is not on disk.
|
|
1548
|
+
let scratchpadDirectory: string | undefined
|
|
1549
|
+
if (!params.sandboxProvider) {
|
|
1550
|
+
try {
|
|
1551
|
+
scratchpadDirectory = ctx.storage.paths?.scratchpadPath(ctx.sessionId)
|
|
1552
|
+
} catch {
|
|
1553
|
+
scratchpadDirectory = undefined
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1545
1556
|
const toolExecutor = ToolingBootstrap.init(
|
|
1546
1557
|
{
|
|
1547
1558
|
skillGrants,
|
|
@@ -1555,6 +1566,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1555
1566
|
...(params.additionalDirectories?.length
|
|
1556
1567
|
? { additionalDirectories: params.additionalDirectories }
|
|
1557
1568
|
: {}),
|
|
1569
|
+
...(scratchpadDirectory ? { scratchpadDirectory } : {}),
|
|
1558
1570
|
...(params.outsideRootAccess ? { outsideRootAccess: params.outsideRootAccess } : {}),
|
|
1559
1571
|
...(params.sandboxEscape ? { sandboxEscape: params.sandboxEscape } : {}),
|
|
1560
1572
|
permissionMode: () => ctx.permissionMode.current,
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
markProviderRejectedImage,
|
|
74
74
|
projectRequestRichContent,
|
|
75
75
|
} from '../request-rich-content.js'
|
|
76
|
+
import { withoutVoidAssistantTurns } from '../request-void-turns.js'
|
|
76
77
|
import { formatSteeringNote, isOperatorUserMessage } from '../steering.js'
|
|
77
78
|
import { effortForModel } from './effort-clamp.js'
|
|
78
79
|
import { parseNativeCandidate } from './native-output.js'
|
|
@@ -929,7 +930,7 @@ export class IterationOrchestrator {
|
|
|
929
930
|
// and a mode change would re-key the whole conversation.
|
|
930
931
|
if (policyNotice) requestHistory.push(stepContextMessage(policyNotice))
|
|
931
932
|
const projected = projectRequestRichContent(
|
|
932
|
-
this.projectObservations(requestHistory),
|
|
933
|
+
withoutVoidAssistantTurns(this.projectObservations(requestHistory)),
|
|
933
934
|
this.ctx.turnConfig.maxRequestRichContentBytes ??
|
|
934
935
|
DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
|
|
935
936
|
{ imagesReadable: this.modelReadsImages(stepModel) },
|
|
@@ -3155,7 +3156,7 @@ export class IterationOrchestrator {
|
|
|
3155
3156
|
...(workingMemory.context ? [workingMemory.context] : []),
|
|
3156
3157
|
]
|
|
3157
3158
|
const finalMessages = projectRequestRichContent(
|
|
3158
|
-
this.projectObservations(finalHistory),
|
|
3159
|
+
withoutVoidAssistantTurns(this.projectObservations(finalHistory)),
|
|
3159
3160
|
this.ctx.turnConfig.maxRequestRichContentBytes ?? DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
|
|
3160
3161
|
{ imagesReadable: this.modelReadsImages(model) },
|
|
3161
3162
|
)
|
|
@@ -238,6 +238,12 @@ export async function* runToolReview(
|
|
|
238
238
|
...(requiresApproval ? { requiresApproval: true as const } : {}),
|
|
239
239
|
...(escalation ? { escalation } : {}),
|
|
240
240
|
...(hookPermission ? { hookPermission } : {}),
|
|
241
|
+
...('scratchpadWrite' in tc && tc.scratchpadWrite
|
|
242
|
+
? { scratchpadWrite: true as const }
|
|
243
|
+
: {}),
|
|
244
|
+
...('scratchpadWrite' in tc && tc.scratchpadWrite
|
|
245
|
+
? { scratchpadWrite: true as const }
|
|
246
|
+
: {}),
|
|
241
247
|
}
|
|
242
248
|
})
|
|
243
249
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Message } from '../../types/message/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The provider-bound view of a conversation without assistant entries that
|
|
5
|
+
* carry nothing at all: no text, no tool call, no reasoning, no citation.
|
|
6
|
+
*
|
|
7
|
+
* An empty reply (a refused or cut-off model, or a completion that produced
|
|
8
|
+
* nothing) is recorded in the history, and the loop goes on to ask again, so
|
|
9
|
+
* the next request would carry it. Chat Completions endpoints reject such an
|
|
10
|
+
* entry ("content or tool_calls must be set"), and no wire gives it a meaning.
|
|
11
|
+
* The stored history is never edited; only the request leaves it out.
|
|
12
|
+
*
|
|
13
|
+
* An entry that still holds reasoning stays: a driver whose wire replays signed
|
|
14
|
+
* or native reasoning needs it, and each driver decides what a reasoning-only
|
|
15
|
+
* entry looks like on its own wire. An entry with tool calls always stays, so a
|
|
16
|
+
* tool result still follows the call it answers. Returns the original array
|
|
17
|
+
* when nothing is dropped.
|
|
18
|
+
*/
|
|
19
|
+
export function withoutVoidAssistantTurns(messages: Message[]): Message[] {
|
|
20
|
+
const isVoid = (message: Message): boolean =>
|
|
21
|
+
message.role === 'assistant' &&
|
|
22
|
+
!message.content &&
|
|
23
|
+
(message.toolCalls?.length ?? 0) === 0 &&
|
|
24
|
+
(message.reasoning?.length ?? 0) === 0 &&
|
|
25
|
+
(message.citations?.length ?? 0) === 0 &&
|
|
26
|
+
(message.textParts?.length ?? 0) === 0
|
|
27
|
+
return messages.some(isVoid) ? messages.filter((message) => !isVoid(message)) : messages
|
|
28
|
+
}
|
|
@@ -1051,8 +1051,19 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1051
1051
|
mode === 'plan' &&
|
|
1052
1052
|
options.planModeWrite?.(tc) === true &&
|
|
1053
1053
|
tc.authorization?.decision !== 'deny'
|
|
1054
|
+
// A file written inside the session's own scratch folder, in any mode but
|
|
1055
|
+
// `strict`: the folder is the session's, so it is not a change anyone
|
|
1056
|
+
// reviews, and not "outside the roots". A rule that asked for review keeps
|
|
1057
|
+
// the call in the batch; a deny still refuses it.
|
|
1058
|
+
const isScratchpadWrite = (tc: ToolCallSummary): boolean =>
|
|
1059
|
+
mode !== 'strict' &&
|
|
1060
|
+
tc.scratchpadWrite === true &&
|
|
1061
|
+
tc.escalation === undefined &&
|
|
1062
|
+
tc.requiresApproval !== true &&
|
|
1063
|
+
!tc.authorization?.explicitReview &&
|
|
1064
|
+
tc.authorization?.decision !== 'deny'
|
|
1054
1065
|
const reviewable = request.toolCalls.filter(
|
|
1055
|
-
(tc) => !(isPlanFileWrite(tc) && !tc.authorization?.explicitReview),
|
|
1066
|
+
(tc) => !(isPlanFileWrite(tc) && !tc.authorization?.explicitReview) && !isScratchpadWrite(tc),
|
|
1056
1067
|
)
|
|
1057
1068
|
if (!batchNeedsReview(reviewable, exempt)) {
|
|
1058
1069
|
return { action: 'approve_tools' }
|
|
@@ -1089,7 +1100,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1089
1100
|
if (
|
|
1090
1101
|
mode === 'plan' &&
|
|
1091
1102
|
!onlyReadsOutsideRoots(
|
|
1092
|
-
reviewable.filter((tc) => !isPlanFileWrite(tc)),
|
|
1103
|
+
reviewable.filter((tc) => !isPlanFileWrite(tc) && !isScratchpadWrite(tc)),
|
|
1093
1104
|
exempt,
|
|
1094
1105
|
)
|
|
1095
1106
|
) {
|
|
@@ -28,6 +28,8 @@ export interface ToolingBootstrapConfig {
|
|
|
28
28
|
workingDirectory: string
|
|
29
29
|
/** See `QueryParams.additionalDirectories`. */
|
|
30
30
|
additionalDirectories?: readonly string[]
|
|
31
|
+
/** See `ToolExecutorConfig.scratchpadDirectory`. */
|
|
32
|
+
scratchpadDirectory?: string
|
|
31
33
|
/** See `QueryParams.outsideRootAccess`. */
|
|
32
34
|
outsideRootAccess?: 'refuse' | 'review'
|
|
33
35
|
/** See `QueryParams.sandboxEscape`. */
|
|
@@ -99,6 +101,7 @@ export class ToolingBootstrap {
|
|
|
99
101
|
...(config.additionalDirectories?.length
|
|
100
102
|
? { additionalDirectories: config.additionalDirectories }
|
|
101
103
|
: {}),
|
|
104
|
+
...(config.scratchpadDirectory ? { scratchpadDirectory: config.scratchpadDirectory } : {}),
|
|
102
105
|
...(config.outsideRootAccess ? { outsideRootAccess: config.outsideRootAccess } : {}),
|
|
103
106
|
...(config.sandboxEscape ? { sandboxEscape: config.sandboxEscape } : {}),
|
|
104
107
|
permissionMode: config.permissionMode,
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
applyEnvironmentOverrides,
|
|
54
54
|
pickEnvironmentEntries,
|
|
55
55
|
} from '../../utils/process-environment.js'
|
|
56
|
+
import { createTempDir, resolveTempBase } from '../../utils/temp.js'
|
|
56
57
|
import { walkFilesLocally } from '../file-walk.js'
|
|
57
58
|
import { assertIsolation, describeIsolation } from '../isolation.js'
|
|
58
59
|
import type { PtyLoader } from '../terminal.js'
|
|
@@ -317,7 +318,7 @@ function detectEnvironment(): DetectedEnvironment {
|
|
|
317
318
|
// hosts that mediate namespace helpers and can claim a tier production
|
|
318
319
|
// cannot actually drive.
|
|
319
320
|
const bwrap = resolveTrustedWrapper('bwrap')
|
|
320
|
-
if (bwrap !== undefined && probeBwrapSpawn(bwrap, buildBwrapArgs(
|
|
321
|
+
if (bwrap !== undefined && probeBwrapSpawn(bwrap, buildBwrapArgs(resolveTempBase()))) {
|
|
321
322
|
return { environment: 'linux-bwrap', wrapperCommand: bwrap }
|
|
322
323
|
}
|
|
323
324
|
|
|
@@ -329,7 +330,10 @@ function detectEnvironment(): DetectedEnvironment {
|
|
|
329
330
|
|
|
330
331
|
if (platform === 'darwin') {
|
|
331
332
|
const seatbelt = resolveTrustedWrapper('sandbox-exec')
|
|
332
|
-
if (
|
|
333
|
+
if (
|
|
334
|
+
seatbelt !== undefined &&
|
|
335
|
+
probeSandboxSpawn(seatbelt, buildSeatbeltArgs(resolveTempBase()))
|
|
336
|
+
) {
|
|
333
337
|
return { environment: 'macos-seatbelt', wrapperCommand: seatbelt }
|
|
334
338
|
}
|
|
335
339
|
}
|
|
@@ -1091,7 +1095,7 @@ class LocalSandbox implements Sandbox {
|
|
|
1091
1095
|
exitCode,
|
|
1092
1096
|
stderr: stderr.text,
|
|
1093
1097
|
timedOut,
|
|
1094
|
-
allowedRoots: [...this.roots, tmpdir(), '/tmp', '/var/tmp', '/dev'],
|
|
1098
|
+
allowedRoots: [...this.roots, tmpdir(), resolveTempBase(), '/tmp', '/var/tmp', '/dev'],
|
|
1095
1099
|
})
|
|
1096
1100
|
resolvePromise({
|
|
1097
1101
|
exitCode,
|
|
@@ -1229,8 +1233,7 @@ export class LocalSandboxProvider implements SandboxProvider {
|
|
|
1229
1233
|
}
|
|
1230
1234
|
} else {
|
|
1231
1235
|
// No workspace was named: this allocation owns a fresh temporary root.
|
|
1232
|
-
const
|
|
1233
|
-
const rawDir = await mkdtemp(join(tmpdir(), SANDBOX_TEMP_DIR_PREFIX))
|
|
1236
|
+
const rawDir = await createTempDir(SANDBOX_TEMP_DIR_PREFIX)
|
|
1234
1237
|
rootDir = canonicalizePath(rawDir)
|
|
1235
1238
|
removeRootOnDestroy = true
|
|
1236
1239
|
}
|
package/src/session/paths.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { lstatSync, mkdirSync } from 'node:fs'
|
|
3
2
|
import { link, mkdir, open, readFile, realpath, unlink } from 'node:fs/promises'
|
|
4
|
-
import { tmpdir, userInfo } from 'node:os'
|
|
5
3
|
import { basename, dirname, join } from 'node:path'
|
|
6
4
|
import type {
|
|
7
5
|
CheckpointId,
|
|
@@ -14,6 +12,12 @@ import type {
|
|
|
14
12
|
} from '../types/ids/index.js'
|
|
15
13
|
import { type ProjectDocument, ProjectDocumentSchema } from '../types/session/records.js'
|
|
16
14
|
import { type EntityIdKind, asSessionId, generateProjectId, isEntityId } from '../utils/id.js'
|
|
15
|
+
import {
|
|
16
|
+
type PrivateTempRootOptions,
|
|
17
|
+
TempDirectoryError,
|
|
18
|
+
privateTempRoot,
|
|
19
|
+
privateTempRootPath,
|
|
20
|
+
} from '../utils/temp.js'
|
|
17
21
|
import { uuidv7 } from '../utils/uuidv7.js'
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -243,68 +247,26 @@ export async function ensureProject(options: EnsureProjectOptions): Promise<Ensu
|
|
|
243
247
|
)
|
|
244
248
|
}
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
readonly tmpdir?: string
|
|
249
|
-
/** Default `process.platform`. */
|
|
250
|
-
readonly platform?: NodeJS.Platform
|
|
251
|
-
/**
|
|
252
|
-
* Default `process.getuid`. Pass `undefined` explicitly to model a
|
|
253
|
-
* platform without it (win32).
|
|
254
|
-
*/
|
|
255
|
-
readonly getuid?: (() => number) | undefined
|
|
256
|
-
/** Default `os.userInfo().username`. */
|
|
257
|
-
readonly username?: () => string
|
|
258
|
-
}
|
|
250
|
+
/** See {@link PrivateTempRootOptions}; `tmpdir` is the base. */
|
|
251
|
+
export type TempRootOptions = PrivateTempRootOptions
|
|
259
252
|
|
|
260
253
|
/**
|
|
261
|
-
* The per-user temporary root,
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
* `%TEMP%` is already per user: a symlink or junction is refused and the
|
|
268
|
-
* owner check is skipped.
|
|
254
|
+
* The per-user temporary root, `<base>/namzu-<user>`, created if missing,
|
|
255
|
+
* where the base is `NAMZU_TMPDIR` or `os.tmpdir()`. A root that is a symlink,
|
|
256
|
+
* not a directory, owned by another uid or open to others is refused with a
|
|
257
|
+
* {@link SessionPathError}; one that cannot be created raises the
|
|
258
|
+
* {@link TempDirectoryError} that names the directory and `NAMZU_TMPDIR`.
|
|
259
|
+
* See {@link privateTempRoot}.
|
|
269
260
|
*/
|
|
270
261
|
export function tempRoot(options: TempRootOptions = {}): string {
|
|
271
|
-
const platform = options.platform ?? process.platform
|
|
272
|
-
const getuid = 'getuid' in options ? options.getuid : process.getuid?.bind(process)
|
|
273
|
-
let user = 'user'
|
|
274
|
-
if (typeof getuid === 'function') {
|
|
275
|
-
user = String(getuid())
|
|
276
|
-
} else {
|
|
277
|
-
try {
|
|
278
|
-
const name = (options.username ?? (() => userInfo().username))()
|
|
279
|
-
if (name.length > 0) user = sha256Hex(name).slice(0, 12)
|
|
280
|
-
} catch {
|
|
281
|
-
// No user database entry: fall back to the fixed segment.
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const root = join(options.tmpdir ?? tmpdir(), `namzu-${user}`)
|
|
285
262
|
try {
|
|
286
|
-
|
|
263
|
+
return privateTempRoot(options)
|
|
287
264
|
} catch (error) {
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
const entry = lstatSync(root)
|
|
291
|
-
if (entry.isSymbolicLink()) {
|
|
292
|
-
throw new SessionPathError(`Refusing temporary root ${root}: it is a symbolic link.`)
|
|
293
|
-
}
|
|
294
|
-
if (!entry.isDirectory()) {
|
|
295
|
-
throw new SessionPathError(`Refusing temporary root ${root}: it is not a directory.`)
|
|
296
|
-
}
|
|
297
|
-
if (platform !== 'win32' && typeof getuid === 'function') {
|
|
298
|
-
if (entry.uid !== getuid()) {
|
|
299
|
-
throw new SessionPathError(`Refusing temporary root ${root}: it belongs to uid ${entry.uid}.`)
|
|
300
|
-
}
|
|
301
|
-
if ((entry.mode & 0o077) !== 0) {
|
|
302
|
-
throw new SessionPathError(
|
|
303
|
-
`Refusing temporary root ${root}: mode ${(entry.mode & 0o777).toString(8)} lets other users in; it must be 700.`,
|
|
304
|
-
)
|
|
265
|
+
if (error instanceof TempDirectoryError && error.reason === 'refused') {
|
|
266
|
+
throw new SessionPathError(error.message)
|
|
305
267
|
}
|
|
268
|
+
throw error
|
|
306
269
|
}
|
|
307
|
-
return root
|
|
308
270
|
}
|
|
309
271
|
|
|
310
272
|
/** Every path of one project's layout. Pure: nothing is created or read. */
|
|
@@ -451,9 +413,22 @@ export class SessionPaths {
|
|
|
451
413
|
return join(this.sessionDir(locator), 'lease.json')
|
|
452
414
|
}
|
|
453
415
|
|
|
454
|
-
/**
|
|
416
|
+
/** `<temp base>/namzu-<user>/<slug>/<session-id>/scratchpad`. Not created; the per-user root is. */
|
|
455
417
|
tempDir(sessionId: SessionId): string {
|
|
456
418
|
this.#tempRoot ??= tempRoot()
|
|
457
419
|
return join(this.#tempRoot, this.slug, checkId(sessionId, 'session'), 'scratchpad')
|
|
458
420
|
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Where {@link tempDir} is, creating and checking nothing, not even the
|
|
424
|
+
* per-user root: for a caller that makes the folder only when it is first used.
|
|
425
|
+
*/
|
|
426
|
+
scratchpadPath(sessionId: SessionId): string {
|
|
427
|
+
return join(
|
|
428
|
+
this.#tempRoot ?? privateTempRootPath(),
|
|
429
|
+
this.slug,
|
|
430
|
+
checkId(sessionId, 'session'),
|
|
431
|
+
'scratchpad',
|
|
432
|
+
)
|
|
433
|
+
}
|
|
459
434
|
}
|