mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(provenance): name every guard that can latch a registry (#6513)
* fix(provenance): name every guard that can latch a registry A production latch reported `reason: "unspecified"` because 44 call sites took the default. The reason is the only thing that names which guard tripped, and a refusal surfaces many frames later as one fixed sentence, so an unnamed latch is undiagnosable — that is what left an incident's origin unidentified for a day. Give each call site a literal that names its guard, add the 20 new literals to the reason union, and sort them into the existing error/warn split: a guard that should not trip on a healthy run reports at error, everything else stays at warn. `log-creation-skipped` joins the by-design set since it fires on every run that does not persist a log. Make `reason` required on both `markIncomplete` and `markInputPathIncomplete`, so omission is a compile error rather than a silent `unspecified`. A caller with genuinely nothing to say now passes `'unspecified'` where a reviewer can see it. The three remaining bare calls are on ResolvedSecretTraceProvenanceAccumulator, a different class with no reason concept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(provenance): pin the new reasons and the guard that latched in production Cover what the reason set is for rather than only that it compiles: the non-enumerable tool-params branch now asserts it names `tool-input-not-enumerable`, which is the guard the production logs showed reporting `unspecified`, and every new literal asserts which stream it reports on — error for a guard that cannot trip on a healthy run, warn for one reachable without a fault, silent for the by-design log-less session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(provenance): make reason the only locator, not one of two Two fields had grown into competing answers to the same question. `origin` is a free-form label for which importer accepted an already-incomplete bundle; four latches had started passing `markIncomplete('unspecified', { origin })`, using it to stand in for a reason that did not exist yet. That splits one fact across a closed enum and an open string, leaving neither worth alerting on. Give those four the literal they were reaching for — none needed a new one — and split the five reasons that covered genuinely different guards, so the reason alone locates the site rather than needing an origin beside it. `origin` keeps its narrow job, now documented: it disambiguates importers that share one guard, and a latch that wants an origin because no reason fits should add a reason instead. No production call site passes 'unspecified' any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(provenance): stop two expected states from reporting as faults Cursor Bugbot caught `backfill-checkpoint-*`: the guard covered four conditions under one reason classified as an originating fault, and one of them — a state persisted before the checkpoint contract existed — is what essentially every legacy row looks like. A backfill over historical rows would have put one error line per row into the stream the error/warn split exists to protect. Auditing the rest of the error-level reasons for the same shape found a second: a client tool invoked without a run id has no binding to unseal against, so it took the `[null, null]` path and reported `client-tool-seal-failed` at error on an ordinary configuration. Split both along the line that matters — absent versus unusable, not attempted versus failed — and classify each half: expected states warn, genuine faults keep error. `backfill-scope-mismatch` is retired; it named one of its four conditions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5f4dc195fb
commit
4bafd14110
@@ -381,7 +381,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
results,
|
||||
})
|
||||
if (!resultProvenanceSnapshot.imported) {
|
||||
resultSecretRegistry.markIncomplete()
|
||||
resultSecretRegistry.markIncomplete('knowledge-result-provenance-unavailable')
|
||||
if (useReranker) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Knowledge result secret provenance is unavailable' },
|
||||
@@ -608,7 +608,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
'knowledge'
|
||||
))
|
||||
) {
|
||||
resultSecretRegistry.markIncomplete()
|
||||
resultSecretRegistry.markIncomplete('knowledge-result-provenance-unavailable')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ export async function createKnowledgeProvenanceResponse(options: {
|
||||
})
|
||||
for (const provenance of options.provenances) {
|
||||
if (provenance.status === 'unknown') {
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('durable-provenance-unknown')
|
||||
break
|
||||
}
|
||||
const sourceRegistry = await createDurableSecretProvenanceRegistry(provenance, {
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function createMemoryResponse(options: {
|
||||
workspaceId: options.workspaceId,
|
||||
})
|
||||
if (options.memories.length > MAX_PRIVATE_MEMORY_CROSSINGS) {
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('memory-crossing-capacity-exceeded')
|
||||
} else {
|
||||
const ids = [...new Set(options.memories.map((record) => record.id))]
|
||||
const memoriesById = new Map<string, MemoryCrossing[]>()
|
||||
@@ -123,7 +123,7 @@ export async function createMemoryResponse(options: {
|
||||
provenanceEntryCount > MAX_PRIVATE_MEMORY_PROVENANCE_ENTRIES ||
|
||||
provenanceBytes > MAX_PRIVATE_MEMORY_PROVENANCE_BYTES
|
||||
) {
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('memory-crossing-capacity-exceeded')
|
||||
break
|
||||
}
|
||||
const sidecarById = new Map(sidecars.map((sidecar) => [sidecar.memoryId, sidecar]))
|
||||
|
||||
@@ -142,7 +142,7 @@ export const POST = withRouteHandler(
|
||||
: undefined
|
||||
const trustedProvenance = trustedExecutionState?.resolvedSecretTraceProvenance
|
||||
if (trustedProvenance === undefined) {
|
||||
resolvedSecretTraceRegistry.markIncomplete()
|
||||
resolvedSecretTraceRegistry.markIncomplete('restored-provenance-untrusted')
|
||||
} else {
|
||||
await resolvedSecretTraceRegistry.importProvenance(trustedProvenance, {
|
||||
trusted: true,
|
||||
|
||||
@@ -278,7 +278,7 @@ describe('Memory', () => {
|
||||
|
||||
it('persists raw memory with unknown lineage when provenance is unavailable', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
const appendMessage = vi
|
||||
.spyOn(memoryService as any, 'appendMessage')
|
||||
.mockResolvedValue(undefined)
|
||||
@@ -293,7 +293,7 @@ describe('Memory', () => {
|
||||
|
||||
it('seeds raw memory with unknown lineage when provenance is unavailable', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
const seedMemoryRecord = vi
|
||||
.spyOn(memoryService as any, 'seedMemoryRecord')
|
||||
.mockResolvedValue(undefined)
|
||||
|
||||
@@ -199,7 +199,8 @@ export class GenericBlockHandler implements BlockHandler {
|
||||
boundary && boundary.paths.length > 0 && registry?.hasResolvedInputProjections()
|
||||
? registry.projectResolvedInputSelections(inputs)
|
||||
: undefined
|
||||
if (projectedInputs?.complete === false) registry?.markIncomplete()
|
||||
if (projectedInputs?.complete === false)
|
||||
registry?.markIncomplete('structural-input-projection-incomplete')
|
||||
|
||||
if (projectedInputs?.complete && boundary && tool && registry) {
|
||||
for (const projection of projectedInputs.values) {
|
||||
@@ -233,7 +234,7 @@ export class GenericBlockHandler implements BlockHandler {
|
||||
continue
|
||||
}
|
||||
if (boundary.requiredProjectionRoots.has(projection.path[0])) {
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('structural-input-root-unprojected')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ async function consumeMothershipProvenance(
|
||||
return false
|
||||
}
|
||||
if (inspection.status === 'invalid') {
|
||||
registry?.markIncomplete()
|
||||
registry?.markIncomplete('mothership-provenance-invalid')
|
||||
throw new Error('Mothership response provenance metadata is invalid')
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ function inspectMothershipResponseCapability(
|
||||
return false
|
||||
}
|
||||
|
||||
registry?.markIncomplete()
|
||||
registry?.markIncomplete('mothership-provenance-invalid')
|
||||
throw new Error('Mothership response provenance metadata is invalid')
|
||||
}
|
||||
|
||||
@@ -464,7 +464,7 @@ async function readMothershipExecuteResponse(
|
||||
result = (await response.json()) as MothershipExecuteResult
|
||||
} catch (error) {
|
||||
if (expectsProvenance) {
|
||||
registry?.markIncomplete()
|
||||
registry?.markIncomplete('mothership-response-unreadable')
|
||||
throw new Error('Mothership response provenance metadata is invalid')
|
||||
}
|
||||
throw error
|
||||
@@ -528,7 +528,7 @@ async function readMothershipExecuteResponse(
|
||||
return finalResult
|
||||
} finally {
|
||||
if (expectsProvenance && !finalResult && !receivedTerminalProvenance) {
|
||||
registry?.markIncomplete()
|
||||
registry?.markIncomplete('mothership-provenance-missing')
|
||||
}
|
||||
reader.releaseLock()
|
||||
}
|
||||
@@ -630,7 +630,7 @@ function createMothershipStreamingExecution(
|
||||
}
|
||||
} finally {
|
||||
if (expectsProvenance && !sawFinal && !receivedTerminalProvenance) {
|
||||
options.registry?.markIncomplete()
|
||||
options.registry?.markIncomplete('mothership-provenance-missing')
|
||||
}
|
||||
cleanup()
|
||||
reader?.releaseLock()
|
||||
@@ -948,7 +948,7 @@ export class MothershipBlockHandler implements BlockHandler {
|
||||
try {
|
||||
payload = (await response.clone().json()) as MothershipExecuteResult
|
||||
} catch {
|
||||
resultRegistry?.markIncomplete()
|
||||
resultRegistry?.markIncomplete('mothership-response-unreadable')
|
||||
throw new Error('Mothership response provenance metadata is invalid')
|
||||
}
|
||||
await consumeMothershipProvenance(payload, response, resultRegistry)
|
||||
|
||||
@@ -320,7 +320,7 @@ describe('buildSimToolSpecs', () => {
|
||||
output: { result: 'untrusted output' },
|
||||
})
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
const [spec] = await buildSimToolSpecs(executionContext(registry), toolInput)
|
||||
|
||||
const result = await spec.execute({})
|
||||
|
||||
@@ -227,7 +227,7 @@ describe('PiBlockHandler', () => {
|
||||
|
||||
it('fails closed when task provenance is incomplete', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
|
||||
await expect(
|
||||
handler.execute(
|
||||
|
||||
@@ -288,7 +288,7 @@ describe('buildPiSearchToolSpec', () => {
|
||||
|
||||
it('fails closed before search when provenance is incomplete', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
|
||||
const result = await buildTool('exa', executionContext(registry)).execute({ query: 'pi' })
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('buildPiSearchToolSpec', () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
const mergeSpy = vi.spyOn(registry, 'mergeToolCallRegistry')
|
||||
mockExecuteTool.mockImplementation(async (_toolId, _params, options) => {
|
||||
options.resolvedSecretTraceRegistry.markIncomplete()
|
||||
options.resolvedSecretTraceRegistry.markIncomplete('unspecified')
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('projectResolvedSecretModelContent', () => {
|
||||
value: '{{TOKEN}}',
|
||||
})
|
||||
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
expect(projectResolvedSecretModelContent('secret-value', registry)).toEqual({ safe: false })
|
||||
expect(projectResolvedSecretModelContent('secret-value', undefined)).toEqual({ safe: false })
|
||||
})
|
||||
@@ -368,7 +368,7 @@ describe('projectResolvedSecretModelJsonContent', () => {
|
||||
|
||||
it('does not invoke JSON serialization when provenance is incomplete', () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
const toJSON = vi.fn(() => ({ value: 'untrusted' }))
|
||||
|
||||
expect(projectResolvedSecretModelJsonContent({ toJSON }, registry)).toEqual({ safe: false })
|
||||
@@ -474,7 +474,7 @@ describe('projectResolvedSecretDiagnosticError', () => {
|
||||
it('falls back to text-free structure when provenance is missing or incomplete', () => {
|
||||
const error = new Error('secret __var_API_KEY')
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
|
||||
expect(projectResolvedSecretDiagnosticError(error, undefined)).toEqual({
|
||||
errorType: 'error',
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('ResolvedSecretTraceProvenanceAccumulator', () => {
|
||||
entries: [{ name: 'TOKEN', encryptedValue: 'encrypted-value' }],
|
||||
scope,
|
||||
})
|
||||
accumulator.markIncomplete()
|
||||
accumulator.markIncomplete('unspecified')
|
||||
expect(accumulator.exportProvenance().entries).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1443,6 +1443,81 @@ describe('incompleteness diagnostics', () => {
|
||||
expect(reasons).not.toContain('value-provenance-filter-incomplete')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'tool-input-not-enumerable',
|
||||
'tool-params-transform-failed',
|
||||
'structural-input-projection-incomplete',
|
||||
'mothership-provenance-invalid',
|
||||
'client-tool-seal-failed',
|
||||
'knowledge-row-missing',
|
||||
'knowledge-row-content-mismatch',
|
||||
'mothership-response-unreadable',
|
||||
'structural-input-root-unprojected',
|
||||
'backfill-checkpoint-unusable',
|
||||
] as const)('reports %s at error, since it cannot trip on a healthy run', (reason) => {
|
||||
new ResolvedSecretTraceRegistry([], scope).markIncomplete(reason)
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'Resolved secret registry marked incomplete',
|
||||
expect.objectContaining({ reason })
|
||||
)
|
||||
expect(mockLogger.warn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
'mothership-provenance-missing',
|
||||
'client-tool-completion-missing',
|
||||
'client-tool-completion-deferred',
|
||||
'client-tool-completion-unidentified',
|
||||
'client-tool-execution-untrusted',
|
||||
'client-tool-content-unavailable',
|
||||
'knowledge-result-provenance-unavailable',
|
||||
'knowledge-response-capacity-exceeded',
|
||||
'memory-crossing-capacity-exceeded',
|
||||
'workspace-scope-missing',
|
||||
'mounted-file-provenance-unavailable',
|
||||
'table-snapshot-unsafe-for-mount',
|
||||
'restored-provenance-untrusted',
|
||||
'backfill-checkpoint-absent',
|
||||
'client-tool-seal-absent',
|
||||
] as const)('reports %s at warn, since it is reachable without a fault', (reason) => {
|
||||
new ResolvedSecretTraceRegistry([], scope).markIncomplete(reason)
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
'Resolved secret registry marked incomplete',
|
||||
expect.objectContaining({ reason })
|
||||
)
|
||||
expect(mockLogger.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
* The taxonomy's rule is that an error reason cannot trip on a healthy run. A backfill walking
|
||||
* historical rows hits the no-checkpoint case on essentially every legacy row, so classifying it
|
||||
* as a fault would put one error line per row into the stream this split exists to protect.
|
||||
*/
|
||||
it('separates an absent checkpoint from an unusable one, so a backfill cannot flood errors', () => {
|
||||
new ResolvedSecretTraceRegistry([], scope).markIncomplete('backfill-checkpoint-absent')
|
||||
expect(mockLogger.error).not.toHaveBeenCalled()
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
'Resolved secret registry marked incomplete',
|
||||
expect.objectContaining({ reason: 'backfill-checkpoint-absent' })
|
||||
)
|
||||
|
||||
vi.clearAllMocks()
|
||||
new ResolvedSecretTraceRegistry([], scope).markIncomplete('backfill-checkpoint-unusable')
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'Resolved secret registry marked incomplete',
|
||||
expect.objectContaining({ reason: 'backfill-checkpoint-unusable' })
|
||||
)
|
||||
})
|
||||
|
||||
it('does not report a log-less session at all, since it fires on every such run', () => {
|
||||
new ResolvedSecretTraceRegistry([], scope).markIncomplete('log-creation-skipped')
|
||||
|
||||
expect(mockLogger.error).not.toHaveBeenCalled()
|
||||
expect(mockLogger.warn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports an incoming incomplete bundle at warn, since no catalog was ever on offer', () => {
|
||||
const registry = new ResolvedSecretTraceRegistry([], scope)
|
||||
|
||||
@@ -1465,7 +1540,7 @@ describe('incompleteness diagnostics', () => {
|
||||
it('keeps an unaudited caller taking the default reason out of the error stream', () => {
|
||||
const registry = new ResolvedSecretTraceRegistry([], scope)
|
||||
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
|
||||
expect(mockLogger.error).not.toHaveBeenCalled()
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
|
||||
@@ -41,6 +41,36 @@ export type ResolvedSecretIncompletenessReason =
|
||||
| 'value-provenance-filter-incomplete'
|
||||
| 'durable-provenance-unknown'
|
||||
| 'durable-provenance-malformed'
|
||||
| 'tool-input-not-enumerable'
|
||||
| 'tool-params-transform-failed'
|
||||
| 'structural-input-projection-incomplete'
|
||||
| 'structural-input-root-unprojected'
|
||||
| 'mothership-provenance-invalid'
|
||||
| 'mothership-response-unreadable'
|
||||
| 'mothership-provenance-missing'
|
||||
| 'client-tool-seal-absent'
|
||||
| 'client-tool-seal-failed'
|
||||
| 'client-tool-completion-missing'
|
||||
| 'client-tool-completion-deferred'
|
||||
| 'client-tool-completion-unidentified'
|
||||
| 'client-tool-execution-untrusted'
|
||||
| 'client-tool-content-unavailable'
|
||||
| 'knowledge-result-provenance-unavailable'
|
||||
| 'knowledge-response-capacity-exceeded'
|
||||
| 'knowledge-row-missing'
|
||||
| 'knowledge-row-content-mismatch'
|
||||
| 'memory-crossing-capacity-exceeded'
|
||||
| 'workspace-scope-missing'
|
||||
| 'mounted-file-provenance-unavailable'
|
||||
| 'table-snapshot-unsafe-for-mount'
|
||||
| 'restored-provenance-untrusted'
|
||||
| 'backfill-checkpoint-absent'
|
||||
| 'backfill-checkpoint-unusable'
|
||||
| 'log-creation-skipped'
|
||||
/**
|
||||
* Only for a caller that has not been given a reason yet. A refusal reporting this names no
|
||||
* guard, which is the state that made a production latch untraceable — prefer adding a literal.
|
||||
*/
|
||||
| 'unspecified'
|
||||
|
||||
/**
|
||||
@@ -68,6 +98,16 @@ const ORIGINATING_FAULT_REASONS = new Set<ResolvedSecretIncompletenessReason>([
|
||||
'value-provenance-untrusted',
|
||||
'value-provenance-import-failed',
|
||||
'durable-provenance-malformed',
|
||||
'tool-input-not-enumerable',
|
||||
'tool-params-transform-failed',
|
||||
'structural-input-projection-incomplete',
|
||||
'mothership-provenance-invalid',
|
||||
'client-tool-seal-failed',
|
||||
'knowledge-row-missing',
|
||||
'knowledge-row-content-mismatch',
|
||||
'mothership-response-unreadable',
|
||||
'structural-input-root-unprojected',
|
||||
'backfill-checkpoint-unusable',
|
||||
])
|
||||
|
||||
/**
|
||||
@@ -77,6 +117,8 @@ const ORIGINATING_FAULT_REASONS = new Set<ResolvedSecretIncompletenessReason>([
|
||||
*/
|
||||
const BY_DESIGN_INCOMPLETENESS_REASONS = new Set<ResolvedSecretIncompletenessReason>([
|
||||
'constructed-incomplete',
|
||||
/** A session that will not persist a log has nothing to vouch for; it fires on every such run. */
|
||||
'log-creation-skipped',
|
||||
])
|
||||
|
||||
/**
|
||||
@@ -188,6 +230,16 @@ type PreparedProvenanceFilterResult =
|
||||
/** Extra attribution for a latch: which registry it propagated from, and which importer caused it. */
|
||||
interface MarkIncompleteContext {
|
||||
source?: ResolvedSecretTraceRegistry
|
||||
/**
|
||||
* Which importer accepted an already-incomplete bundle — only meaningful where several callers
|
||||
* share one guard, as {@link ImportResolvedSecretTraceProvenanceOptions.origin} describes.
|
||||
*
|
||||
* It is not a second way to say what `reason` says. A latch that reaches for an origin because no
|
||||
* reason fits is the signal to add a reason literal instead: `reason` is a closed set that can be
|
||||
* alerted on and aggregated, and splitting the same fact across two fields leaves neither
|
||||
* trustworthy. Passing `'unspecified'` alongside an origin is the shape that produced a
|
||||
* production latch naming no guard at all.
|
||||
*/
|
||||
origin?: string
|
||||
}
|
||||
|
||||
@@ -1418,8 +1470,15 @@ export class ResolvedSecretTraceRegistry {
|
||||
return !this.complete || this.incompleteInputPaths.size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* `reason` is required. It defaulted to `'unspecified'`, and every caller that took the default
|
||||
* produced a latch naming no guard — which is exactly the state that made a production incident
|
||||
* untraceable for a day. Making omission a compile error is what keeps the reason set honest as
|
||||
* new guards are added; a caller that genuinely has nothing to say passes `'unspecified'` on
|
||||
* purpose, where a reviewer can see it.
|
||||
*/
|
||||
markIncomplete(
|
||||
reason: ResolvedSecretIncompletenessReason = 'unspecified',
|
||||
reason: ResolvedSecretIncompletenessReason,
|
||||
context: MarkIncompleteContext = {}
|
||||
): void {
|
||||
if (context.source) this.inheritIncompletenessReasonsFrom(context.source)
|
||||
@@ -1870,7 +1929,7 @@ export class ResolvedSecretTraceRegistry {
|
||||
|
||||
private markInputPathIncomplete(
|
||||
path: ResolvedSecretInputPath | undefined,
|
||||
reason: ResolvedSecretIncompletenessReason = 'unspecified',
|
||||
reason: ResolvedSecretIncompletenessReason,
|
||||
origin?: string
|
||||
): void {
|
||||
if (!path || path.length === 0) {
|
||||
|
||||
@@ -235,7 +235,7 @@ export async function prePersistClientExecutableToolCall(
|
||||
toolInput: data.arguments,
|
||||
})
|
||||
} catch (error) {
|
||||
execContext.resolvedSecretTraceRegistry.markIncomplete()
|
||||
execContext.resolvedSecretTraceRegistry.markIncomplete('client-tool-seal-failed')
|
||||
logger.warn('Failed to seal client tool provenance', {
|
||||
toolCallId: data.toolCallId,
|
||||
error: getErrorMessage(error),
|
||||
|
||||
@@ -950,7 +950,7 @@ describe('runCopilotLifecycle', () => {
|
||||
|
||||
it('does not block ordinary initial Go payloads on unrelated incomplete provenance', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
|
||||
const result = await runCopilotLifecycle(
|
||||
{ message: 'possibly secret', messageId: 'stream-incomplete-projection' },
|
||||
|
||||
@@ -91,8 +91,14 @@ export async function waitForClientToolCompletion({
|
||||
const finishPendingActivation = toolRegistry?.beginPendingActivation()
|
||||
let content: Awaited<ReturnType<typeof unsealClientToolCompletion>> = null
|
||||
try {
|
||||
/**
|
||||
* A tool invoked without a run id has no binding to unseal against, which is a configuration
|
||||
* rather than a fault. Tracking whether unsealing was even attempted keeps that ordinary case
|
||||
* out of the error stream while a genuine unseal failure stays in it.
|
||||
*/
|
||||
const sealingAttempted = Boolean(binding && registry && toolRegistry && registryCanImport)
|
||||
const [sealedContent, sealedContext] =
|
||||
binding && registry && toolRegistry && registryCanImport
|
||||
sealingAttempted && binding && registry
|
||||
? await Promise.all([
|
||||
unsealClientToolCompletion(completion.data, binding),
|
||||
unsealClientToolContext(completion.data, binding, registry),
|
||||
@@ -100,7 +106,9 @@ export async function waitForClientToolCompletion({
|
||||
: [null, null]
|
||||
if (toolRegistry && registryCanImport) {
|
||||
if (!sealedContent || !sealedContext) {
|
||||
toolRegistry.markIncomplete()
|
||||
toolRegistry.markIncomplete(
|
||||
sealingAttempted ? 'client-tool-seal-failed' : 'client-tool-seal-absent'
|
||||
)
|
||||
} else {
|
||||
const imported = await toolRegistry.importProvenance(sealedContext.provenance, {
|
||||
origin: 'copilotToolClient.sealedContext',
|
||||
@@ -116,7 +124,9 @@ export async function waitForClientToolCompletion({
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
toolRegistry?.markIncomplete('unspecified', { origin: 'copilotToolClient.sealedContext' })
|
||||
toolRegistry?.markIncomplete('client-tool-seal-failed', {
|
||||
origin: 'copilotToolClient.sealedContext',
|
||||
})
|
||||
} finally {
|
||||
finishPendingActivation?.()
|
||||
}
|
||||
@@ -243,18 +253,18 @@ export async function waitForWorkflowToolCompletion({
|
||||
try {
|
||||
completion = await waitForToolCompletion(toolCallId, timeoutMs, abortSignal)
|
||||
if (!completion) {
|
||||
toolRegistry?.markIncomplete()
|
||||
toolRegistry?.markIncomplete('client-tool-completion-missing')
|
||||
return null
|
||||
}
|
||||
|
||||
const executionId = getWorkflowToolCompletionExecutionId(completion.data)
|
||||
const deploymentError = getAsyncWorkflowDeploymentError(completion.data)
|
||||
if (completion.status === ASYNC_TOOL_CONFIRMATION_STATUS.background) {
|
||||
toolRegistry?.markIncomplete()
|
||||
toolRegistry?.markIncomplete('client-tool-completion-deferred')
|
||||
return structuralWorkflowCompletion(completion.status, workflowId, executionId)
|
||||
}
|
||||
if (!workflowId || !executionId) {
|
||||
toolRegistry?.markIncomplete()
|
||||
toolRegistry?.markIncomplete('client-tool-completion-unidentified')
|
||||
const structuralStatus =
|
||||
completion.status === MothershipStreamV1ToolOutcome.success
|
||||
? MothershipStreamV1ToolOutcome.error
|
||||
@@ -279,12 +289,12 @@ export async function waitForWorkflowToolCompletion({
|
||||
}
|
||||
|
||||
if (!trustedExecution) {
|
||||
toolRegistry?.markIncomplete()
|
||||
toolRegistry?.markIncomplete('client-tool-execution-untrusted')
|
||||
return structuralWorkflowCompletion(completion.status, workflowId, executionId)
|
||||
}
|
||||
|
||||
if (!trustedExecution.contentAvailable) {
|
||||
toolRegistry?.markIncomplete()
|
||||
toolRegistry?.markIncomplete('client-tool-content-unavailable')
|
||||
return structuralWorkflowCompletion(
|
||||
getWorkflowToolConfirmationStatus(trustedExecution.status),
|
||||
workflowId,
|
||||
@@ -297,7 +307,8 @@ export async function waitForWorkflowToolCompletion({
|
||||
toolRegistry.isPermanentlyIncomplete() ||
|
||||
!trustedExecution.provenance.complete
|
||||
) {
|
||||
if (!trustedExecution.provenance.complete) toolRegistry?.markIncomplete()
|
||||
if (!trustedExecution.provenance.complete)
|
||||
toolRegistry?.markIncomplete('source-provenance-incomplete')
|
||||
return structuralWorkflowCompletion(
|
||||
getWorkflowToolConfirmationStatus(trustedExecution.status),
|
||||
workflowId,
|
||||
@@ -317,9 +328,9 @@ export async function waitForWorkflowToolCompletion({
|
||||
},
|
||||
{ trusted: true }
|
||||
)
|
||||
if (!imported) toolRegistry.markIncomplete()
|
||||
if (!imported) toolRegistry.markIncomplete('value-provenance-import-failed')
|
||||
} catch (error) {
|
||||
toolRegistry.markIncomplete()
|
||||
toolRegistry.markIncomplete('value-provenance-import-failed')
|
||||
logger.warn('Failed to import bound workflow provenance', {
|
||||
toolCallId,
|
||||
workflowId,
|
||||
|
||||
@@ -237,7 +237,7 @@ describe('executeToolAndReport provenance isolation', () => {
|
||||
_params: Record<string, unknown>,
|
||||
toolContext: ExecutionContext
|
||||
) => {
|
||||
toolContext.resolvedSecretTraceRegistry?.markIncomplete()
|
||||
toolContext.resolvedSecretTraceRegistry?.markIncomplete('unspecified')
|
||||
return { success: true, output: { value: 'secret-value' } }
|
||||
}
|
||||
)
|
||||
@@ -270,7 +270,7 @@ describe('executeToolAndReport provenance isolation', () => {
|
||||
_params: Record<string, unknown>,
|
||||
toolContext: ExecutionContext
|
||||
) => {
|
||||
toolContext.resolvedSecretTraceRegistry?.markIncomplete()
|
||||
toolContext.resolvedSecretTraceRegistry?.markIncomplete('unspecified')
|
||||
throw new Error('secret-value')
|
||||
}
|
||||
)
|
||||
@@ -300,7 +300,7 @@ describe('executeToolAndReport provenance isolation', () => {
|
||||
_params: Record<string, unknown>,
|
||||
toolContext: ExecutionContext
|
||||
) => {
|
||||
toolContext.resolvedSecretTraceRegistry?.markIncomplete()
|
||||
toolContext.resolvedSecretTraceRegistry?.markIncomplete('unspecified')
|
||||
abortController.abort()
|
||||
return { success: true, output: { value: 'secret-value' } }
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ describe('projectToolResultForCopilot', () => {
|
||||
'incomplete',
|
||||
(() => {
|
||||
const registry = createRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
return registry
|
||||
})(),
|
||||
],
|
||||
|
||||
@@ -379,7 +379,7 @@ describe('maybeWriteOutputToTable', () => {
|
||||
|
||||
it('persists raw rows with unknown provenance when lineage is incomplete', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
|
||||
const result = await maybeWriteOutputToTable(
|
||||
FunctionExecute.id,
|
||||
@@ -678,7 +678,7 @@ describe('maybeWriteReadCsvToTable', () => {
|
||||
|
||||
it('imports raw CSV rows with unknown provenance when lineage is incomplete', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
|
||||
const result = await maybeWriteReadCsvToTable(
|
||||
ReadTool.id,
|
||||
|
||||
@@ -118,9 +118,9 @@ async function importMountedWorkspaceFileProvenance(args: {
|
||||
},
|
||||
registry: args.registry,
|
||||
})
|
||||
if (!imported) args.registry.markIncomplete()
|
||||
if (!imported) args.registry.markIncomplete('mounted-file-provenance-unavailable')
|
||||
} catch {
|
||||
args.registry.markIncomplete()
|
||||
args.registry.markIncomplete('mounted-file-provenance-unavailable')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,9 +464,10 @@ export async function resolveInputFiles(
|
||||
workspaceId,
|
||||
rowsVersion: snapshot.version,
|
||||
})
|
||||
if (!safeForModelMount) resolvedSecretTraceRegistry.markIncomplete()
|
||||
if (!safeForModelMount)
|
||||
resolvedSecretTraceRegistry.markIncomplete('table-snapshot-unsafe-for-mount')
|
||||
} catch {
|
||||
resolvedSecretTraceRegistry.markIncomplete()
|
||||
resolvedSecretTraceRegistry.markIncomplete('table-snapshot-unsafe-for-mount')
|
||||
}
|
||||
|
||||
if (hasCloudStorage()) {
|
||||
@@ -538,7 +539,7 @@ export async function resolveInputFiles(
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
resolvedSecretTraceRegistry.markIncomplete('unspecified', {
|
||||
resolvedSecretTraceRegistry.markIncomplete('source-provenance-incomplete', {
|
||||
origin: 'copilotFunctionExecute.result',
|
||||
})
|
||||
}
|
||||
@@ -572,9 +573,13 @@ async function importMountedProvenance(
|
||||
trusted: true,
|
||||
})
|
||||
if (!imported)
|
||||
target.markIncomplete('unspecified', { origin: 'copilotFunctionExecute.crossing' })
|
||||
target.markIncomplete('value-provenance-import-failed', {
|
||||
origin: 'copilotFunctionExecute.crossing',
|
||||
})
|
||||
} catch {
|
||||
target.markIncomplete('unspecified', { origin: 'copilotFunctionExecute.crossing' })
|
||||
target.markIncomplete('value-provenance-import-failed', {
|
||||
origin: 'copilotFunctionExecute.crossing',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
|
||||
results,
|
||||
})
|
||||
if (!resultProvenance.imported) {
|
||||
resultRegistry.markIncomplete()
|
||||
resultRegistry.markIncomplete('knowledge-result-provenance-unavailable')
|
||||
throw new Error('Knowledge result secret provenance is unavailable')
|
||||
}
|
||||
|
||||
|
||||
@@ -427,7 +427,7 @@ async function importRowsForModel(
|
||||
const registry = context.resolvedSecretTraceRegistry
|
||||
if (!registry) return
|
||||
if (!context.workspaceId) {
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('workspace-scope-missing')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ describe('Knowledge model input provenance', () => {
|
||||
|
||||
it('fails before model egress when an active request registry is incomplete', () => {
|
||||
const registry = new ResolvedSecretTraceRegistry([])
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
|
||||
expect(() =>
|
||||
runWithKnowledgeModelInputProvenance(registry, () =>
|
||||
|
||||
@@ -458,7 +458,7 @@ export async function importKnowledgePersistedResponseSecretProvenance(options:
|
||||
documents.length > MAX_KNOWLEDGE_RESPONSE_PROVENANCE_ROWS ||
|
||||
chunks.length > MAX_KNOWLEDGE_RESPONSE_PROVENANCE_ROWS
|
||||
) {
|
||||
options.registry.markIncomplete()
|
||||
options.registry.markIncomplete('knowledge-response-capacity-exceeded')
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -480,14 +480,14 @@ export async function importKnowledgePersistedResponseSecretProvenance(options:
|
||||
const documentById = new Map(documentRows.map((row) => [row.id, row]))
|
||||
const chunkById = new Map(chunkRows.map((row) => [row.id, row]))
|
||||
if (documentById.size !== documentIds.length || chunkById.size !== chunkIds.length) {
|
||||
options.registry.markIncomplete()
|
||||
options.registry.markIncomplete('knowledge-row-missing')
|
||||
return false
|
||||
}
|
||||
|
||||
for (const item of documents) {
|
||||
const row = documentById.get(item.id)
|
||||
if (!row) {
|
||||
options.registry.markIncomplete()
|
||||
options.registry.markIncomplete('knowledge-row-missing')
|
||||
return false
|
||||
}
|
||||
const source = createKnowledgeDocumentSourceValue(row)
|
||||
@@ -496,7 +496,7 @@ export async function importKnowledgePersistedResponseSecretProvenance(options:
|
||||
createKnowledgeDocumentSourceValue(item.source)
|
||||
)
|
||||
if (!actualSourceHash || !expectedSourceHash || actualSourceHash !== expectedSourceHash) {
|
||||
options.registry.markIncomplete()
|
||||
options.registry.markIncomplete('knowledge-row-content-mismatch')
|
||||
return false
|
||||
}
|
||||
const provenance = filterKnowledgeDocumentMetadataSecretProvenance(
|
||||
@@ -513,7 +513,7 @@ export async function importKnowledgePersistedResponseSecretProvenance(options:
|
||||
for (const item of chunks) {
|
||||
const row = chunkById.get(item.id)
|
||||
if (!row || row.documentId !== item.documentId || row.content !== item.content) {
|
||||
options.registry.markIncomplete()
|
||||
options.registry.markIncomplete('knowledge-row-content-mismatch')
|
||||
return false
|
||||
}
|
||||
const provenance = readBoundKnowledgeEmbeddingSecretProvenance(row)
|
||||
|
||||
@@ -311,7 +311,7 @@ export class LoggingSession {
|
||||
|
||||
if (!isResolvedSecretTraceProvenanceV1(provenance)) {
|
||||
const incomplete = new ResolvedSecretTraceRegistry()
|
||||
incomplete.markIncomplete()
|
||||
incomplete.markIncomplete('restored-provenance-untrusted')
|
||||
return incomplete
|
||||
}
|
||||
|
||||
@@ -781,7 +781,7 @@ export class LoggingSession {
|
||||
[],
|
||||
scopeUserId ? { userId: scopeUserId, workspaceId } : undefined
|
||||
)
|
||||
if (skipLogCreation) this.resolvedSecretTraceRegistry.markIncomplete()
|
||||
if (skipLogCreation) this.resolvedSecretTraceRegistry.markIncomplete('log-creation-skipped')
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('resolveAutoModel', () => {
|
||||
|
||||
it('does not gate caller-projected signals on ambient registry completeness', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
mockFetchGo.mockResolvedValue(routerResponse({ choice: '1' }))
|
||||
|
||||
const result = await resolveAutoModel({
|
||||
|
||||
@@ -72,14 +72,24 @@ export async function createBackfillExecutionSecretRegistry(options: {
|
||||
? options.executionData.executionState
|
||||
: undefined
|
||||
const provenance = state?.resolvedSecretTraceProvenance
|
||||
/**
|
||||
* A state persisted before the checkpoint contract carries no version at all. That is the bulk of
|
||||
* any backfill over historical rows, so it is separated from a checkpoint that exists but cannot
|
||||
* be used — only the latter is worth an error, and conflating them would put one line per legacy
|
||||
* row into the error stream.
|
||||
*/
|
||||
const checkpointPresent =
|
||||
state?.resolvedSecretTraceCheckpointVersion === RESOLVED_SECRET_TRACE_CHECKPOINT_VERSION
|
||||
const valid =
|
||||
state?.resolvedSecretTraceCheckpointVersion === RESOLVED_SECRET_TRACE_CHECKPOINT_VERSION &&
|
||||
checkpointPresent &&
|
||||
state?.sourceExecutionId === options.executionId &&
|
||||
isResolvedSecretTraceProvenanceV1(provenance) &&
|
||||
provenance.scope?.workspaceId === options.workspaceId
|
||||
const registry = new ResolvedSecretTraceRegistry([], valid ? provenance.scope : undefined)
|
||||
if (!valid) {
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete(
|
||||
checkpointPresent ? 'backfill-checkpoint-unusable' : 'backfill-checkpoint-absent'
|
||||
)
|
||||
return registry
|
||||
}
|
||||
await registry.importProvenance(provenance, {
|
||||
|
||||
@@ -539,7 +539,7 @@ async function executeWorkflowCoreImpl(
|
||||
scope: { userId: personalEnvUserId, workspaceId: providedWorkspaceId },
|
||||
})
|
||||
if (restoredState && !restoreTrusted) {
|
||||
resolvedSecretTraceRegistry.markIncomplete()
|
||||
resolvedSecretTraceRegistry.markIncomplete('restored-provenance-untrusted')
|
||||
}
|
||||
if (options.trustedInitialResolvedSecretTraceProvenance !== undefined) {
|
||||
await resolvedSecretTraceRegistry.importProvenance(
|
||||
|
||||
@@ -1288,7 +1288,7 @@ describe('executeProviderRequest — caller-prepared model input', () => {
|
||||
|
||||
it('does not make provider execution depend on registry completeness', async () => {
|
||||
const incomplete = new ResolvedSecretTraceRegistry()
|
||||
incomplete.markIncomplete()
|
||||
incomplete.markIncomplete('unspecified')
|
||||
|
||||
await executeProviderRequest(
|
||||
'anthropic',
|
||||
|
||||
@@ -527,7 +527,7 @@ describe('provider runtime context', () => {
|
||||
{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' },
|
||||
])
|
||||
mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => {
|
||||
options.resolvedSecretTraceRegistry?.markIncomplete()
|
||||
options.resolvedSecretTraceRegistry?.markIncomplete('unspecified')
|
||||
return { success: true, output: { value: 'secret-value' } }
|
||||
})
|
||||
|
||||
@@ -546,7 +546,7 @@ describe('provider runtime context', () => {
|
||||
{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' },
|
||||
])
|
||||
mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => {
|
||||
options.resolvedSecretTraceRegistry?.markIncomplete()
|
||||
options.resolvedSecretTraceRegistry?.markIncomplete('unspecified')
|
||||
return {
|
||||
success: false,
|
||||
output: { value: 'secret-value' },
|
||||
@@ -600,7 +600,7 @@ describe('provider runtime context', () => {
|
||||
{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' },
|
||||
])
|
||||
mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => {
|
||||
options.resolvedSecretTraceRegistry?.markIncomplete()
|
||||
options.resolvedSecretTraceRegistry?.markIncomplete('unspecified')
|
||||
throw new Error('secret-value')
|
||||
})
|
||||
|
||||
@@ -628,7 +628,7 @@ describe('provider runtime context', () => {
|
||||
const registry = new ResolvedSecretTraceRegistry([
|
||||
{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' },
|
||||
])
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
mockExecuteTool.mockResolvedValueOnce({
|
||||
success: true,
|
||||
output: { value: 'secret-value' },
|
||||
|
||||
@@ -1570,7 +1570,7 @@ export function prepareToolExecution(
|
||||
try {
|
||||
projectedToolParams = tool.paramsTransform(projectedToolParams)
|
||||
} catch {
|
||||
inputRegistry.markIncomplete()
|
||||
inputRegistry.markIncomplete('tool-params-transform-failed')
|
||||
projectedToolParams = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1924,7 +1924,7 @@ describe('executeTool Function', () => {
|
||||
|
||||
it('runs a private-provenance call from an incomplete parent without replacing its result', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
registry.markIncomplete()
|
||||
registry.markIncomplete('unspecified')
|
||||
const fetchMock = vi.mocked(global.fetch)
|
||||
|
||||
const result = await executeTool(
|
||||
@@ -1938,10 +1938,26 @@ describe('executeTool Function', () => {
|
||||
expect(fetchMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
* The shape that latched in production with `reason: "unspecified"` — a getter or symbol key on
|
||||
* the params record makes the input lineage unboundable, and the fork is marked before the tool
|
||||
* runs. Pinned by name so a refusal downstream can be traced back to this guard.
|
||||
*/
|
||||
it('names the guard when tool params are not enumerable plain data', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
const params: Record<string, unknown> = { code: 'return "unreachable"' }
|
||||
Object.defineProperty(params, 'envVars', { enumerable: true, get: () => ({}) })
|
||||
|
||||
await executeTool('function_execute', params, { resolvedSecretTraceRegistry: registry })
|
||||
|
||||
expect(registry.isComplete()).toBe(false)
|
||||
expect(registry.getIncompletenessDiagnostics()?.reasons).toContain('tool-input-not-enumerable')
|
||||
})
|
||||
|
||||
it('runs a private-provenance call when its input lineage cannot be bounded', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry()
|
||||
const incompleteToolRegistry = registry.forkForToolCall()
|
||||
incompleteToolRegistry.markIncomplete()
|
||||
incompleteToolRegistry.markIncomplete('unspecified')
|
||||
vi.spyOn(registry, 'forkForInputPaths').mockReturnValue(incompleteToolRegistry)
|
||||
const fetchMock = vi.mocked(global.fetch)
|
||||
|
||||
|
||||
@@ -1408,7 +1408,7 @@ export async function executeTool(
|
||||
const toolRegistry = paramEntries
|
||||
? parentRegistry.forkForInputPaths(paramEntries.map(([key]) => [key] as const))
|
||||
: parentRegistry.forkForToolCall()
|
||||
if (!paramEntries) toolRegistry.markIncomplete()
|
||||
if (!paramEntries) toolRegistry.markIncomplete('tool-input-not-enumerable')
|
||||
const executionContext = options.executionContext
|
||||
? { ...options.executionContext, resolvedSecretTraceRegistry: toolRegistry }
|
||||
: undefined
|
||||
|
||||
Reference in New Issue
Block a user