@agent-native/core 0.210.0-nightly-20261019153056 → 0.210.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/LICENSE +21 -0
- package/dist/agent/action-error-details.d.ts +26 -0
- package/dist/agent/action-error-details.js +99 -0
- package/dist/agent/execution-fence.d.ts +9 -2
- package/dist/agent/execution-fence.js +42 -12
- package/dist/agent/production-agent.js +9 -0
- package/dist/agent/run-manager.js +14 -14
- package/dist/agent/run-store.js +8 -6
- package/dist/agent/tool-error-redaction.d.ts +8 -1
- package/dist/agent/tool-error-redaction.js +70 -7
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/actions/create-org-service-token.js +6 -3
- package/dist/mcp/actions/set-service-principal-lifecycle.js +1 -0
- package/dist/mcp/actions/set-service-principal-policy.js +1 -0
- package/dist/mcp/direct-action-call.js +39 -10
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/workspace-services.js +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/templates/chat/.agents/skills/actions/SKILL.md +2 -2
- package/dist/templates/chat/netlify.toml +0 -1
- package/dist/templates/default/.agents/skills/actions/SKILL.md +2 -2
- package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +44 -21
- package/dist/templates/headless/.agents/skills/actions/SKILL.md +2 -2
- package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +2 -2
- package/docs/content/actions-defining.mdx +2 -2
- package/docs/content/template-chat-agent.mdx +88 -0
- package/docs/content/template-chat-developers.mdx +586 -104
- package/docs/content/template-chat-features.mdx +158 -0
- package/docs/content/template-chat-integrations.mdx +66 -0
- package/docs/content/template-chat.mdx +36 -166
- package/docs/content/template-clips-developers.mdx +1 -1
- package/docs/content/template-clips-integrations.mdx +50 -5
- package/package.json +4 -4
- package/src/templates/chat/.agents/skills/actions/SKILL.md +2 -2
- package/src/templates/chat/netlify.toml +0 -1
- package/src/templates/default/.agents/skills/actions/SKILL.md +2 -2
- package/src/templates/factory/.agents/skills/review-prs/SKILL.md +44 -21
- package/src/templates/headless/.agents/skills/actions/SKILL.md +2 -2
- package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +2 -2
- package/docs/content/template-chat-first-edits.mdx +0 -158
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Builder.io, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Longest serialized `details` an agent sees with a failed action. */
|
|
2
|
+
export declare const MAX_AGENT_ERROR_DETAILS_CHARS = 2000;
|
|
3
|
+
export interface AgentErrorDetails {
|
|
4
|
+
/** The `details` a `fail()` attached, as the browser receives them, redacted and bounded. */
|
|
5
|
+
details: Record<string, unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Top-level keys left out to stay within the bound, or because they don't
|
|
8
|
+
* serialize: up to ten names, each clipped, with the full count beside them.
|
|
9
|
+
*/
|
|
10
|
+
omittedKeys?: string[];
|
|
11
|
+
omittedKeyCount?: number;
|
|
12
|
+
/** The line a text-only tool result carries, so text clients see the same data. */
|
|
13
|
+
line: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The structured `details` of a failed action, in the form an agent reads.
|
|
17
|
+
*
|
|
18
|
+
* Only a contract error (`fail()`) carries details meant for the caller; the
|
|
19
|
+
* HTTP action route already returns them to the browser, so an agent acting
|
|
20
|
+
* for the same user gets the same object. Values go through a JSON round trip
|
|
21
|
+
* first, so a `Date` reads as the browser would see it rather than as `{}`.
|
|
22
|
+
* When the whole object is too long, small top-level entries are kept in
|
|
23
|
+
* order and the rest are named in `omittedKeys`: a retry id must survive next
|
|
24
|
+
* to a large payload, and a cut must never read as a complete object.
|
|
25
|
+
*/
|
|
26
|
+
export declare function agentErrorDetails(error: unknown): AgentErrorDetails | undefined;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { isActionContractError } from "../action.js";
|
|
2
|
+
import { redactSensitiveFields, sanitizeToolErrorText, } from "./tool-error-redaction.js";
|
|
3
|
+
/** Longest serialized `details` an agent sees with a failed action. */
|
|
4
|
+
export const MAX_AGENT_ERROR_DETAILS_CHARS = 2_000;
|
|
5
|
+
/** A top-level entry longer than this is left out once the whole set is too long. */
|
|
6
|
+
const MAX_KEPT_ENTRY_CHARS = 300;
|
|
7
|
+
const MAX_LISTED_OMITTED_KEYS = 10;
|
|
8
|
+
const MAX_OMITTED_KEY_CHARS = 60;
|
|
9
|
+
/**
|
|
10
|
+
* The structured `details` of a failed action, in the form an agent reads.
|
|
11
|
+
*
|
|
12
|
+
* Only a contract error (`fail()`) carries details meant for the caller; the
|
|
13
|
+
* HTTP action route already returns them to the browser, so an agent acting
|
|
14
|
+
* for the same user gets the same object. Values go through a JSON round trip
|
|
15
|
+
* first, so a `Date` reads as the browser would see it rather than as `{}`.
|
|
16
|
+
* When the whole object is too long, small top-level entries are kept in
|
|
17
|
+
* order and the rest are named in `omittedKeys`: a retry id must survive next
|
|
18
|
+
* to a large payload, and a cut must never read as a complete object.
|
|
19
|
+
*/
|
|
20
|
+
export function agentErrorDetails(error) {
|
|
21
|
+
if (!isActionContractError(error))
|
|
22
|
+
return undefined;
|
|
23
|
+
const raw = error.details;
|
|
24
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
25
|
+
return undefined;
|
|
26
|
+
const entries = [];
|
|
27
|
+
const omittedKeys = [];
|
|
28
|
+
for (const key of Object.keys(raw)) {
|
|
29
|
+
const clean = jsonSafeEntry(raw, key);
|
|
30
|
+
if (clean === UNSERIALIZABLE)
|
|
31
|
+
omittedKeys.push(key);
|
|
32
|
+
else if (clean !== undefined)
|
|
33
|
+
entries.push([key, clean]);
|
|
34
|
+
}
|
|
35
|
+
const cleaned = Object.fromEntries(entries);
|
|
36
|
+
let details = cleaned;
|
|
37
|
+
if (JSON.stringify(cleaned).length > MAX_AGENT_ERROR_DETAILS_CHARS) {
|
|
38
|
+
details = {};
|
|
39
|
+
let budget = MAX_AGENT_ERROR_DETAILS_CHARS - 2;
|
|
40
|
+
for (const [key, value] of entries) {
|
|
41
|
+
const size = JSON.stringify({ [key]: value }).length - 1;
|
|
42
|
+
if (size <= MAX_KEPT_ENTRY_CHARS && size <= budget) {
|
|
43
|
+
details[key] = value;
|
|
44
|
+
budget -= size;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
omittedKeys.push(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (Object.keys(details).length === 0 && omittedKeys.length === 0) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const listed = omittedKeys
|
|
55
|
+
.slice(0, MAX_LISTED_OMITTED_KEYS)
|
|
56
|
+
.map((key) => key.length > MAX_OMITTED_KEY_CHARS
|
|
57
|
+
? `${key.slice(0, MAX_OMITTED_KEY_CHARS)}…`
|
|
58
|
+
: key);
|
|
59
|
+
const more = omittedKeys.length > listed.length
|
|
60
|
+
? `, and ${omittedKeys.length - listed.length} more`
|
|
61
|
+
: "";
|
|
62
|
+
return {
|
|
63
|
+
details,
|
|
64
|
+
...(omittedKeys.length > 0
|
|
65
|
+
? { omittedKeys: listed, omittedKeyCount: omittedKeys.length }
|
|
66
|
+
: {}),
|
|
67
|
+
line: `Error details${listed.length > 0 ? ` (omitted: ${listed.join(", ")}${more})` : ""}: ${JSON.stringify(details)}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const UNSERIALIZABLE = Symbol("unserializable");
|
|
71
|
+
function jsonSafeEntry(raw, key) {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
// A getter can throw. Reading it here names the key; reading it outside
|
|
75
|
+
// would replace the action's own error.
|
|
76
|
+
const json = JSON.stringify(raw[key]);
|
|
77
|
+
if (json === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
parsed = JSON.parse(json);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return UNSERIALIZABLE;
|
|
83
|
+
}
|
|
84
|
+
const redacted = redactSensitiveFields({ [key]: parsed }, { compoundNames: true });
|
|
85
|
+
return sanitizeStrings(redacted[key]);
|
|
86
|
+
}
|
|
87
|
+
function sanitizeStrings(value) {
|
|
88
|
+
if (typeof value === "string")
|
|
89
|
+
return sanitizeToolErrorText(value);
|
|
90
|
+
if (Array.isArray(value))
|
|
91
|
+
return value.map(sanitizeStrings);
|
|
92
|
+
if (value && typeof value === "object") {
|
|
93
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
94
|
+
key,
|
|
95
|
+
sanitizeStrings(entry),
|
|
96
|
+
]));
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
@@ -5,11 +5,18 @@ export interface ExecutionFence {
|
|
|
5
5
|
runId: string;
|
|
6
6
|
generation: string;
|
|
7
7
|
}
|
|
8
|
-
export declare class
|
|
9
|
-
readonly code
|
|
8
|
+
export declare class ExecutionFenceError extends Error {
|
|
9
|
+
readonly code: string;
|
|
10
|
+
constructor(code: string, message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class StaleExecutionFenceError extends ExecutionFenceError {
|
|
10
13
|
constructor(fence: ExecutionFence);
|
|
11
14
|
}
|
|
15
|
+
export declare class MissingExecutionFenceError extends ExecutionFenceError {
|
|
16
|
+
constructor();
|
|
17
|
+
}
|
|
12
18
|
export declare function currentExecutionFence(): ExecutionFence | undefined;
|
|
19
|
+
export declare function withExecutionLeaseRequested<T>(work: () => T): T;
|
|
13
20
|
export declare function withExecutionFence<T>(fence: ExecutionFence, work: () => T): T;
|
|
14
21
|
export declare function lockAndAssertCurrentExecutionFences(tx: DbExec): Promise<void>;
|
|
15
22
|
export declare function withFencedExecutionWrite<T>(write: (tx: DbExec) => Promise<T>, scope?: {
|
|
@@ -1,27 +1,53 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { getDbExec } from "../db/client.js";
|
|
3
|
-
export class
|
|
4
|
-
code
|
|
3
|
+
export class ExecutionFenceError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = new.target.name;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class StaleExecutionFenceError extends ExecutionFenceError {
|
|
5
12
|
constructor(fence) {
|
|
6
|
-
super(`Execution lease generation ${fence.generation} for turn ${fence.turnId} is no longer current`);
|
|
7
|
-
|
|
13
|
+
super("stale_execution_fence", `Execution lease generation ${fence.generation} for turn ${fence.turnId} is no longer current`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class MissingExecutionFenceError extends ExecutionFenceError {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("missing_execution_fence", "Execution lease was requested without an execution fence");
|
|
8
19
|
}
|
|
9
20
|
}
|
|
10
21
|
const executionFenceStorage = new AsyncLocalStorage();
|
|
11
22
|
export function currentExecutionFence() {
|
|
12
|
-
const
|
|
13
|
-
|
|
23
|
+
const context = executionFenceStorage.getStore();
|
|
24
|
+
const fence = context?.fences.at(-1);
|
|
25
|
+
if (!fence && context?.executionLeaseRequested) {
|
|
26
|
+
throw new MissingExecutionFenceError();
|
|
27
|
+
}
|
|
28
|
+
return fence;
|
|
29
|
+
}
|
|
30
|
+
export function withExecutionLeaseRequested(work) {
|
|
31
|
+
const current = executionFenceStorage.getStore();
|
|
32
|
+
return executionFenceStorage.run({
|
|
33
|
+
fences: current?.fences ?? [],
|
|
34
|
+
executionLeaseRequested: true,
|
|
35
|
+
}, work);
|
|
14
36
|
}
|
|
15
37
|
export function withExecutionFence(fence, work) {
|
|
16
|
-
const current = executionFenceStorage.getStore()
|
|
17
|
-
const
|
|
38
|
+
const current = executionFenceStorage.getStore();
|
|
39
|
+
const fences = current?.fences ?? [];
|
|
40
|
+
const sameFence = fences.some((candidate) => candidate.threadId === fence.threadId &&
|
|
18
41
|
candidate.turnId === fence.turnId &&
|
|
19
42
|
candidate.runId === fence.runId &&
|
|
20
43
|
candidate.generation === fence.generation);
|
|
21
|
-
return executionFenceStorage.run(
|
|
44
|
+
return executionFenceStorage.run({
|
|
45
|
+
fences: sameFence ? fences : [...fences, fence],
|
|
46
|
+
executionLeaseRequested: true,
|
|
47
|
+
}, work);
|
|
22
48
|
}
|
|
23
49
|
export async function lockAndAssertCurrentExecutionFences(tx) {
|
|
24
|
-
const fences = executionFenceStorage.getStore() ?? [];
|
|
50
|
+
const fences = executionFenceStorage.getStore()?.fences ?? [];
|
|
25
51
|
for (const activeFence of fences) {
|
|
26
52
|
const { rows } = await tx.execute({
|
|
27
53
|
sql: `SELECT generation
|
|
@@ -44,11 +70,15 @@ export async function lockAndAssertCurrentExecutionFences(tx) {
|
|
|
44
70
|
}
|
|
45
71
|
}
|
|
46
72
|
export async function withFencedExecutionWrite(write, scope) {
|
|
47
|
-
const
|
|
73
|
+
const context = executionFenceStorage.getStore();
|
|
74
|
+
const fences = context?.fences ?? [];
|
|
48
75
|
const fence = fences.at(-1);
|
|
49
76
|
const client = getDbExec();
|
|
50
|
-
if (!fence)
|
|
77
|
+
if (!fence) {
|
|
78
|
+
if (context?.executionLeaseRequested)
|
|
79
|
+
throw new MissingExecutionFenceError();
|
|
51
80
|
return write(client);
|
|
81
|
+
}
|
|
52
82
|
// A nested task/run write can target either active scope; both leases still gate it.
|
|
53
83
|
const scopedFence = fences.find((candidate) => (!scope?.runId || scope.runId === candidate.runId) &&
|
|
54
84
|
(!scope?.threadId || scope.threadId === candidate.threadId) &&
|
|
@@ -56,6 +56,7 @@ import { DurableAttachmentReferenceRequiredError, stripInlineBytes, } from "../s
|
|
|
56
56
|
import { isReasoningEffort, normalizeReasoningEffortForRequest, stepDownReasoningEffort, } from "../shared/reasoning-effort.js";
|
|
57
57
|
import { SYNTHETIC_TRAFFIC_BETA_E2E, SYNTHETIC_TRAFFIC_HEADER, } from "../shared/test-traffic.js";
|
|
58
58
|
import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
|
|
59
|
+
import { agentErrorDetails } from "./action-error-details.js";
|
|
59
60
|
import { drainAgentWarnings, formatAgentWarningsForToolResult, } from "./action-warnings.js";
|
|
60
61
|
import { approvalKeyWithArguments } from "./approval-key.js";
|
|
61
62
|
import { CONTINUE_UNAVAILABLE_CODE, } from "./auto-continue.js";
|
|
@@ -6617,6 +6618,7 @@ export async function runAgentLoop(opts) {
|
|
|
6617
6618
|
// handler returned does not.
|
|
6618
6619
|
let toolResultLost = false;
|
|
6619
6620
|
let toolErrorCode;
|
|
6621
|
+
let toolErrorDetailsLine = "";
|
|
6620
6622
|
let mcpApp;
|
|
6621
6623
|
let toolResultImages;
|
|
6622
6624
|
let toolArtifacts = [];
|
|
@@ -6928,6 +6930,7 @@ export async function runAgentLoop(opts) {
|
|
|
6928
6930
|
toolErrorCode = isActionContractError(err)
|
|
6929
6931
|
? err.errorCode
|
|
6930
6932
|
: undefined;
|
|
6933
|
+
toolErrorDetailsLine = agentErrorDetails(err)?.line ?? "";
|
|
6931
6934
|
result = signal.aborted
|
|
6932
6935
|
? INTERRUPTED_TOOL_RESULT_MARKER
|
|
6933
6936
|
: thrownToolResult(err);
|
|
@@ -6940,7 +6943,13 @@ export async function runAgentLoop(opts) {
|
|
|
6940
6943
|
if (isError) {
|
|
6941
6944
|
receipt = undefined;
|
|
6942
6945
|
if (result !== INTERRUPTED_TOOL_RESULT_MARKER) {
|
|
6946
|
+
// Details go on after the loop breakers and the precondition
|
|
6947
|
+
// stop read the result: a retry id that changes on every attempt
|
|
6948
|
+
// must not make the same failure look like a new one.
|
|
6943
6949
|
result = finalizeToolErrorResult(result);
|
|
6950
|
+
if (toolErrorDetailsLine) {
|
|
6951
|
+
result = `${result}\n${toolErrorDetailsLine}`;
|
|
6952
|
+
}
|
|
6944
6953
|
}
|
|
6945
6954
|
}
|
|
6946
6955
|
else {
|
|
@@ -10,7 +10,7 @@ import { classifyTerminalErrorCode, describeErrorWithCauses, isProviderConnectio
|
|
|
10
10
|
import { runErrorTelemetryProperties } from "./engine/error-telemetry.js";
|
|
11
11
|
import { getAgentEngineEntry } from "./engine/registry.js";
|
|
12
12
|
import { EngineError } from "./engine/types.js";
|
|
13
|
-
import { withExecutionFence } from "./execution-fence.js";
|
|
13
|
+
import { MissingExecutionFenceError, withExecutionFence, withExecutionLeaseRequested, } from "./execution-fence.js";
|
|
14
14
|
import { insertRun, insertRunEvent, insertRunEvents, updateRunStatusIfRunning, markRunAborted, markRunAbortedAtSafeBoundary, getRunAbortState, getRunStatus, getRunEventsSince, getCurrentTurnRunEventsForThread, getRunById, getRunByThread, getRunTurnRef, markTurnAborted, cleanupOldRuns, updateRunHeartbeat, bumpRunProgress, setRunInFlightMarker, reapIfStale, reapUnclaimedBackgroundRun, shouldRedispatchUnclaimedBackgroundRun, reconcileTerminalRunFromEvents, ensureTerminalRunEvent, getLastTerminalRunEvent, resolveErroredRunTerminalEvent, setRunError, setRunTerminalReason, persistRunCheckpointEvent, recordRunDiagnostic, RUN_DIAG_STAGE, claimTurnExecutionLease, renewTurnExecutionLease, releaseTurnExecutionLease, TurnExecutionLeaseBusyError, terminalEventForAbortReason, RUN_RECORD_MISSING_ERROR_EVENT, RUN_RECORD_MISSING_GRACE_MS, RUN_TERMINAL_LOOKUP_FAILED_ERROR_EVENT, UNKNOWN_RUN_STATUS_ERROR_EVENT, } from "./run-store.js";
|
|
15
15
|
import { isContinuationTerminalReason } from "./types.js";
|
|
16
16
|
const activeRuns = new Map();
|
|
@@ -670,10 +670,12 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
670
670
|
const withWorkerFence = async (work) => {
|
|
671
671
|
if (!options?.executionLease)
|
|
672
672
|
return work();
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
673
|
+
return withExecutionLeaseRequested(async () => {
|
|
674
|
+
const fence = await executionFencePromise;
|
|
675
|
+
if (!fence)
|
|
676
|
+
throw new MissingExecutionFenceError();
|
|
677
|
+
return withExecutionFence(fence, work);
|
|
678
|
+
});
|
|
677
679
|
};
|
|
678
680
|
let persistenceChain = Promise.resolve();
|
|
679
681
|
run.flushEvents = () => persistenceChain;
|
|
@@ -754,7 +756,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
754
756
|
progressWritePending = false;
|
|
755
757
|
lastProgressBumpAt = Date.now();
|
|
756
758
|
progressWriteAttempts += 1;
|
|
757
|
-
const write = writeProgress
|
|
759
|
+
const write = withWorkerFence(writeProgress)
|
|
758
760
|
.catch((error) => {
|
|
759
761
|
recordProgressWriteFailure(error, "error");
|
|
760
762
|
progressWritePending = true;
|
|
@@ -906,7 +908,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
906
908
|
const recordRunBoundaryDiagnostic = (reason, diagnostic, disposition) => {
|
|
907
909
|
if (getCurrentRequestDbPoolScope()?.closing)
|
|
908
910
|
return;
|
|
909
|
-
void retainRequestDbPoolScope(recordRunDiagnostic(runId, RUN_DIAG_STAGE.runBoundaryReached, JSON.stringify({
|
|
911
|
+
void retainRequestDbPoolScope(withWorkerFence(() => recordRunDiagnostic(runId, RUN_DIAG_STAGE.runBoundaryReached, JSON.stringify({
|
|
910
912
|
reason,
|
|
911
913
|
disposition,
|
|
912
914
|
silentForMs: diagnostic.silentForMs,
|
|
@@ -914,7 +916,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
914
916
|
inFlightWorkCount,
|
|
915
917
|
eventCount: run.events.length,
|
|
916
918
|
elapsedMs: Date.now() - run.startedAt,
|
|
917
|
-
})).catch(() => { })).catch(() => { });
|
|
919
|
+
})).catch(() => { }))).catch(() => { });
|
|
918
920
|
};
|
|
919
921
|
const reachRunBoundary = (reason, diagnostic = {}) => {
|
|
920
922
|
if (run.status !== "running" || abort.signal.aborted)
|
|
@@ -1261,15 +1263,13 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1261
1263
|
};
|
|
1262
1264
|
const startDriver = async () => {
|
|
1263
1265
|
if (options?.executionLease) {
|
|
1264
|
-
const fence = await executionFencePromise;
|
|
1265
1266
|
if (executionLeaseClaimFailed || abort.signal.aborted)
|
|
1266
1267
|
return;
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
else {
|
|
1268
|
+
await withWorkerFence(async () => {
|
|
1269
|
+
if (executionLeaseClaimFailed || abort.signal.aborted)
|
|
1270
|
+
return;
|
|
1271
1271
|
await runFn(send, runControl.chunkSignal, runControl);
|
|
1272
|
-
}
|
|
1272
|
+
});
|
|
1273
1273
|
return;
|
|
1274
1274
|
}
|
|
1275
1275
|
if (servicePrincipalRunStart) {
|
package/dist/agent/run-store.js
CHANGED
|
@@ -14,7 +14,7 @@ import { approvalKeyForHistory } from "./approval-key.js";
|
|
|
14
14
|
import { admitAutoContinue, admitManualContinue, } from "./auto-continue.js";
|
|
15
15
|
import { AGENT_CHAT_RECOVERY_OF_RUN_FIELD } from "./durable-background.js";
|
|
16
16
|
import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./engine/credential-errors.js";
|
|
17
|
-
import { currentExecutionFence, lockAndAssertCurrentExecutionFences, StaleExecutionFenceError, withFencedExecutionWrite, } from "./execution-fence.js";
|
|
17
|
+
import { currentExecutionFence, ExecutionFenceError, MissingExecutionFenceError, lockAndAssertCurrentExecutionFences, StaleExecutionFenceError, withFencedExecutionWrite, } from "./execution-fence.js";
|
|
18
18
|
import { AGENT_RUN_MIGRATIONS, AGENT_RUN_MIGRATIONS_TABLE, } from "./run-migrations.js";
|
|
19
19
|
import { isConsistentToolCallInputFingerprint, isUnavailableToolCallInput, toolCallInputFingerprint, } from "./tool-call-journal.js";
|
|
20
20
|
import { CONTINUATION_REASONS, isContinuationTerminalReason } from "./types.js";
|
|
@@ -788,7 +788,7 @@ export async function writeLedgerEntry(threadId, toolKey, resultSummary, artifac
|
|
|
788
788
|
}), { threadId });
|
|
789
789
|
}
|
|
790
790
|
catch (error) {
|
|
791
|
-
if (error instanceof
|
|
791
|
+
if (error instanceof ExecutionFenceError)
|
|
792
792
|
throw error;
|
|
793
793
|
// Ledger is best-effort; never surface failures to the caller.
|
|
794
794
|
}
|
|
@@ -874,7 +874,9 @@ export async function clearLedgerForThread(threadId) {
|
|
|
874
874
|
args: [threadId],
|
|
875
875
|
}), { threadId });
|
|
876
876
|
}
|
|
877
|
-
catch {
|
|
877
|
+
catch (error) {
|
|
878
|
+
if (error instanceof MissingExecutionFenceError)
|
|
879
|
+
throw error;
|
|
878
880
|
// Best-effort.
|
|
879
881
|
}
|
|
880
882
|
}
|
|
@@ -1489,7 +1491,7 @@ export async function setRunError(runId, errorCode, errorDetail) {
|
|
|
1489
1491
|
}), { runId });
|
|
1490
1492
|
}
|
|
1491
1493
|
catch (error) {
|
|
1492
|
-
if (error instanceof
|
|
1494
|
+
if (error instanceof ExecutionFenceError)
|
|
1493
1495
|
throw error;
|
|
1494
1496
|
// Diagnostics are best-effort; never let them break completion.
|
|
1495
1497
|
}
|
|
@@ -1518,7 +1520,7 @@ export async function setRunTerminalReason(runId, terminalReason) {
|
|
|
1518
1520
|
}), { runId });
|
|
1519
1521
|
}
|
|
1520
1522
|
catch (error) {
|
|
1521
|
-
if (error instanceof
|
|
1523
|
+
if (error instanceof ExecutionFenceError)
|
|
1522
1524
|
throw error;
|
|
1523
1525
|
// Diagnostics are best-effort; never let them break completion.
|
|
1524
1526
|
}
|
|
@@ -1766,7 +1768,7 @@ export async function recordRunDiagnostic(runId, stage, detail) {
|
|
|
1766
1768
|
}
|
|
1767
1769
|
}
|
|
1768
1770
|
catch (error) {
|
|
1769
|
-
if (error instanceof
|
|
1771
|
+
if (error instanceof ExecutionFenceError)
|
|
1770
1772
|
throw error;
|
|
1771
1773
|
// Diagnostics are best-effort; never let them break the run or the route.
|
|
1772
1774
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `compoundNames` also redacts names such as `clientSecret`, `x-api-key`,
|
|
3
|
+
* `githubToken` or `embedTicket`.
|
|
4
|
+
* Tool-error text leaves it off and keeps names like `mySecret` readable.
|
|
5
|
+
*/
|
|
6
|
+
export declare function redactSensitiveFields(value: unknown, options?: {
|
|
7
|
+
compoundNames?: boolean;
|
|
8
|
+
}): unknown;
|
|
2
9
|
export declare function sanitizeToolErrorText(value: string): string;
|
|
3
10
|
export declare function sanitizeToolErrorValue(value: unknown): string;
|
|
@@ -1,22 +1,85 @@
|
|
|
1
|
+
import { EMBED_RESULT_SENSITIVE_KEYS } from "../mcp/embed-redaction.js";
|
|
1
2
|
const REDACTED = "[REDACTED]";
|
|
2
3
|
const SENSITIVE_FIELD_PATTERN = /^(authorization|cookie|api[_-]?key|password|secret|token|access[_-]?token|refresh[_-]?token|bearer)$/i;
|
|
4
|
+
// Only the trailing words of a compound name count. A `…Token` is a
|
|
5
|
+
// credential unless it is a cursor such as `nextPageToken`, which an agent
|
|
6
|
+
// needs for a retry. A `…Ticket` can redeem an embed session.
|
|
7
|
+
const SENSITIVE_LAST_WORDS = new Set([
|
|
8
|
+
"authorization",
|
|
9
|
+
"cookie",
|
|
10
|
+
"credential",
|
|
11
|
+
"credentials",
|
|
12
|
+
"passphrase",
|
|
13
|
+
"password",
|
|
14
|
+
"secret",
|
|
15
|
+
"secrets",
|
|
16
|
+
"ticket",
|
|
17
|
+
]);
|
|
18
|
+
const SENSITIVE_LAST_WORD_PAIRS = new Set([
|
|
19
|
+
"access key",
|
|
20
|
+
"api key",
|
|
21
|
+
"private key",
|
|
22
|
+
"secret key",
|
|
23
|
+
]);
|
|
24
|
+
const CURSOR_TOKEN_QUALIFIERS = new Set([
|
|
25
|
+
"continuation",
|
|
26
|
+
"cursor",
|
|
27
|
+
"delta",
|
|
28
|
+
"idempotency",
|
|
29
|
+
"next",
|
|
30
|
+
"page",
|
|
31
|
+
"prev",
|
|
32
|
+
"previous",
|
|
33
|
+
"resume",
|
|
34
|
+
"skip",
|
|
35
|
+
"sync",
|
|
36
|
+
]);
|
|
37
|
+
function isSensitiveCompoundFieldName(key) {
|
|
38
|
+
if (SENSITIVE_FIELD_PATTERN.test(key) ||
|
|
39
|
+
EMBED_RESULT_SENSITIVE_KEYS.has(key)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
const words = key
|
|
43
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.split(/[^a-z0-9]+/)
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
const last = words.at(-1);
|
|
48
|
+
if (!last)
|
|
49
|
+
return false;
|
|
50
|
+
if (SENSITIVE_LAST_WORDS.has(last))
|
|
51
|
+
return true;
|
|
52
|
+
if (last === "token") {
|
|
53
|
+
return words.length > 1 && !CURSOR_TOKEN_QUALIFIERS.has(words.at(-2));
|
|
54
|
+
}
|
|
55
|
+
return (words.length > 1 && SENSITIVE_LAST_WORD_PAIRS.has(`${words.at(-2)} ${last}`));
|
|
56
|
+
}
|
|
3
57
|
const OPENAI_LIKE_SECRET_PATTERN = /(^|[^A-Za-z0-9_-])(sk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{24,})(?=$|[^A-Za-z0-9_-])/g;
|
|
4
|
-
|
|
5
|
-
|
|
58
|
+
/**
|
|
59
|
+
* `compoundNames` also redacts names such as `clientSecret`, `x-api-key`,
|
|
60
|
+
* `githubToken` or `embedTicket`.
|
|
61
|
+
* Tool-error text leaves it off and keeps names like `mySecret` readable.
|
|
62
|
+
*/
|
|
63
|
+
export function redactSensitiveFields(value, options = {}) {
|
|
64
|
+
const isSensitive = options.compoundNames
|
|
65
|
+
? isSensitiveCompoundFieldName
|
|
66
|
+
: (key) => SENSITIVE_FIELD_PATTERN.test(key);
|
|
67
|
+
return redactWalk(value, new WeakSet(), isSensitive);
|
|
6
68
|
}
|
|
7
|
-
function redactWalk(value, seen) {
|
|
69
|
+
function redactWalk(value, seen, isSensitive) {
|
|
8
70
|
if (value === null || typeof value !== "object")
|
|
9
71
|
return value;
|
|
10
72
|
if (seen.has(value))
|
|
11
73
|
return "[Circular]";
|
|
12
74
|
seen.add(value);
|
|
13
|
-
if (Array.isArray(value))
|
|
14
|
-
return value.map((v) => redactWalk(v, seen));
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
return value.map((v) => redactWalk(v, seen, isSensitive));
|
|
77
|
+
}
|
|
15
78
|
const out = {};
|
|
16
79
|
for (const [key, entry] of Object.entries(value)) {
|
|
17
|
-
out[key] =
|
|
80
|
+
out[key] = isSensitive(key)
|
|
18
81
|
? REDACTED
|
|
19
|
-
: redactWalk(entry, seen);
|
|
82
|
+
: redactWalk(entry, seen, isSensitive);
|
|
20
83
|
}
|
|
21
84
|
return out;
|
|
22
85
|
}
|
|
@@ -38,19 +38,19 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
38
38
|
error: string;
|
|
39
39
|
states?: undefined;
|
|
40
40
|
} | {
|
|
41
|
+
error?: undefined;
|
|
41
42
|
states: {
|
|
42
43
|
clientId: number;
|
|
43
44
|
state: string;
|
|
44
45
|
}[];
|
|
45
|
-
error?: undefined;
|
|
46
46
|
}>>;
|
|
47
47
|
export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
48
48
|
error: string;
|
|
49
49
|
users?: undefined;
|
|
50
50
|
} | {
|
|
51
|
+
error?: undefined;
|
|
51
52
|
users: {
|
|
52
53
|
clientId: number;
|
|
53
54
|
lastSeen: number;
|
|
54
55
|
}[];
|
|
55
|
-
error?: undefined;
|
|
56
56
|
}>>;
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -21,8 +21,8 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
|
|
|
21
21
|
}>>;
|
|
22
22
|
export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
23
23
|
ok?: undefined;
|
|
24
|
-
error: string;
|
|
25
24
|
text?: undefined;
|
|
25
|
+
error: string;
|
|
26
26
|
} | {
|
|
27
27
|
error?: undefined;
|
|
28
28
|
ok: boolean;
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* - Gated to org owners/admins (see service-token-access.ts).
|
|
10
10
|
* - The token value appears ONLY in this action's response — it is never
|
|
11
11
|
* stored (only its `jti`) and never logged.
|
|
12
|
-
* - Not
|
|
13
|
-
*
|
|
14
|
-
* never a
|
|
12
|
+
* - Not an agent or MCP tool (`agentTool: false`), and not callable from
|
|
13
|
+
* extensions or run-code (`toolCallable: false`): minting a long-lived
|
|
14
|
+
* credential must be an explicit human/HTTP/CLI act, never a
|
|
15
|
+
* prompt-injection target. `toolCallable: false` alone does not keep it
|
|
16
|
+
* out of the agent's tool list.
|
|
15
17
|
* - Revocable via `revoke-org-service-token` — same `revoked_at` gate the
|
|
16
18
|
* personal-token revocation path uses.
|
|
17
19
|
*
|
|
@@ -54,6 +56,7 @@ export default defineAction({
|
|
|
54
56
|
allowedActions: allowedActionsSchema.nullable().optional(),
|
|
55
57
|
}),
|
|
56
58
|
requiresAuth: true,
|
|
59
|
+
agentTool: false,
|
|
57
60
|
toolCallable: false,
|
|
58
61
|
audit: orgAdminAudit({
|
|
59
62
|
targetType: "service-principal",
|