@sema-agent/core 7.50.2 → 7.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +263 -0
- package/dist/agents/child-failure.d.ts +5 -16
- package/dist/agents/child-failure.js +4 -24
- package/dist/agents/subagent-steps.js +2 -2
- package/dist/agents/subagent.d.ts +4 -13
- package/dist/agents/subagent.js +94 -204
- package/dist/agents/tool-filter.d.ts +2 -2
- package/dist/brain/anthropic.d.ts +1 -1
- package/dist/brain/anthropic.js +7 -5
- package/dist/brain/open-responses.d.ts +1 -1
- package/dist/brain/open-responses.js +3 -2
- package/dist/brain/openai.d.ts +1 -1
- package/dist/brain/openai.js +3 -2
- package/dist/brain/request-route.d.ts +2 -2
- package/dist/brain/route-adjudicator.d.ts +16 -7
- package/dist/brain/route-adjudicator.js +23 -7
- package/dist/brain/route-conformance.js +23 -5
- package/dist/brain/stream-engine.js +9 -3
- package/dist/core/a2a.js +6 -8
- package/dist/core/allow-entry.d.ts +45 -26
- package/dist/core/allow-entry.js +48 -24
- package/dist/core/arg-summary.d.ts +8 -85
- package/dist/core/arg-summary.js +4 -386
- package/dist/core/ask-origin.d.ts +35 -33
- package/dist/core/ask-origin.js +9 -11
- package/dist/core/checkpoint-requirements.d.ts +6 -0
- package/dist/core/checkpoint-requirements.js +1 -0
- package/dist/core/checkpoint-store.d.ts +15 -34
- package/dist/core/checkpoint-store.js +5 -4
- package/dist/core/classifier-unavailable-arm.d.ts +12 -8
- package/dist/core/classifier-unavailable-arm.js +7 -2
- package/dist/core/credential-vocabulary.d.ts +3 -2
- package/dist/core/credential-vocabulary.js +27 -18
- package/dist/core/delegation-frames.d.ts +40 -15
- package/dist/core/delegation-frames.js +34 -0
- package/dist/core/engine-notice.d.ts +35 -11
- package/dist/core/gate-fold.js +9 -7
- package/dist/core/gate-lanes.js +6 -4
- package/dist/core/gate-outcome.d.ts +81 -15
- package/dist/core/gate-outcome.js +168 -67
- package/dist/core/governance-codes.d.ts +7 -1
- package/dist/core/governance-codes.js +9 -0
- package/dist/core/hooks.d.ts +4 -4
- package/dist/core/hooks.js +4 -3
- package/dist/core/mcp-connection-pool.d.ts +2 -0
- package/dist/core/mcp-connection-pool.js +2 -1
- package/dist/core/mcp-failure.js +2 -1
- package/dist/core/mcp-server-spec.d.ts +27 -5
- package/dist/core/mcp.d.ts +13 -38
- package/dist/core/mcp.js +28 -23
- package/dist/core/media-byte-cap.d.ts +25 -0
- package/dist/core/media-byte-cap.js +18 -2
- package/dist/core/memory-engine/consolidation-driver.js +1 -1
- package/dist/core/mode-policy.d.ts +25 -1
- package/dist/core/mode-policy.js +28 -13
- package/dist/core/own-property-receipt.d.ts +18 -0
- package/dist/core/own-property-receipt.js +39 -0
- package/dist/core/permission-rule-model.d.ts +32 -11
- package/dist/core/permission-rule-model.js +12 -7
- package/dist/core/persisted-rule-arms.d.ts +3 -3
- package/dist/core/prompt-section-seat.d.ts +54 -0
- package/dist/core/prompt-section-seat.js +49 -0
- package/dist/core/protocol-http-auth.d.ts +44 -0
- package/dist/core/protocol-http-auth.js +45 -0
- package/dist/core/read-face-seats.d.ts +30 -0
- package/dist/core/read-face-seats.js +38 -0
- package/dist/core/redaction-passes.d.ts +82 -0
- package/dist/core/redaction-passes.js +378 -0
- package/dist/core/remote-env.d.ts +4 -3
- package/dist/core/resume-startup-cause.d.ts +56 -0
- package/dist/core/resume-startup-cause.js +43 -0
- package/dist/core/rule-denial-sentence.d.ts +2 -2
- package/dist/core/runner/announce-once-ledger.js +2 -2
- package/dist/core/runner/classifier-ladder.d.ts +17 -13
- package/dist/core/runner/classifier-ladder.js +12 -10
- package/dist/core/runner/construction-doors.js +9 -20
- package/dist/core/runner/contracts.d.ts +4 -4
- package/dist/core/runner/gate-exit.js +2 -2
- package/dist/core/runner/memory-consolidation-write-judge.d.ts +53 -9
- package/dist/core/runner/memory-consolidation-write-judge.js +39 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +8 -13
- package/dist/core/runner/person-path-rows.d.ts +4 -3
- package/dist/core/runner/policy-throw.js +4 -4
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +8 -6
- package/dist/core/runner/prepare-gate-stations.js +4 -4
- package/dist/core/runner/prepare-hands-readface.js +1 -19
- package/dist/core/runner/prepare-inherited-gate.js +10 -9
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -5
- package/dist/core/runner/prepare-policy-chain.js +10 -13
- package/dist/core/runner/prepare-project-context.d.ts +4 -4
- package/dist/core/runner/prepare-project-context.js +2 -1
- package/dist/core/runner/prepare-prompt-assembly.js +1 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -1
- package/dist/core/runner/prepare-prompt-inputs.js +4 -4
- package/dist/core/runner/prepare-protocol-tools.d.ts +4 -3
- package/dist/core/runner/prepare-protocol-tools.js +3 -2
- package/dist/core/runner/prepare-task.js +6 -4
- package/dist/core/runner/prepare-turn-wiring.d.ts +16 -13
- package/dist/core/runner/prepare-turn-wiring.js +8 -7
- package/dist/core/runner/run-harness-handlers.js +2 -2
- package/dist/core/runner/run-leg.js +10 -9
- package/dist/core/runner/run-settle-and-teardown.js +5 -5
- package/dist/core/runner/run-turn-boundary.js +3 -2
- package/dist/core/runner/runtask.d.ts +1 -2
- package/dist/core/runner/runtask.js +8 -12
- package/dist/core/runner/session-rule-policy.d.ts +15 -11
- package/dist/core/runner/session-rule-policy.js +43 -33
- package/dist/core/runner/stream-lifecycle-verbs.js +2 -2
- package/dist/core/runner/stream-settle-backstop.js +2 -1
- package/dist/core/runner/sub-agent-deny-lists.d.ts +12 -8
- package/dist/core/runner/sub-agent-deny-lists.js +23 -12
- package/dist/core/runner/terminal-account.js +3 -3
- package/dist/core/runner/tool-output-projection.js +3 -8
- package/dist/core/runner/tool-result-media-seat.d.ts +19 -0
- package/dist/core/runner/tool-result-media-seat.js +13 -0
- package/dist/core/runner/turn-attachments.d.ts +13 -12
- package/dist/core/runner/turn-attachments.js +11 -7
- package/dist/core/runner-deps.d.ts +4 -3
- package/dist/core/safety-merge-corpus.js +21 -21
- package/dist/core/secret-env.js +1 -1
- package/dist/core/session-policy-store.d.ts +89 -72
- package/dist/core/session-policy-store.js +100 -93
- package/dist/core/shell-read-silence.d.ts +38 -4
- package/dist/core/shell-read-silence.js +102 -0
- package/dist/core/side-query.d.ts +7 -8
- package/dist/core/skill-grant-binding.d.ts +32 -11
- package/dist/core/skill-grant-binding.js +11 -3
- package/dist/core/skill-grant.d.ts +4 -3
- package/dist/core/skill-grant.js +5 -4
- package/dist/core/skills-directory.d.ts +12 -1
- package/dist/core/skills-directory.js +17 -8
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +3 -0
- package/dist/core/swappable-deps.d.ts +15 -1
- package/dist/core/swappable-deps.js +25 -1
- package/dist/core/synthetic-read-verdict.d.ts +16 -14
- package/dist/core/synthetic-read-verdict.js +28 -8
- package/dist/core/task-notification.d.ts +13 -3
- package/dist/core/task-notification.js +12 -1
- package/dist/core/task-spec.d.ts +14 -15
- package/dist/core/tool-name-readings.d.ts +0 -4
- package/dist/core/tool-name-readings.js +0 -1
- package/dist/core/tool-policy.d.ts +40 -9
- package/dist/core/tool-policy.js +25 -7
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/core/untrusted-egress.d.ts +60 -11
- package/dist/core/untrusted-egress.js +120 -9
- package/dist/core/untrusted-text.js +16 -0
- package/dist/core/wiring-manifest.d.ts +22 -8
- package/dist/core/wiring-manifest.js +12 -4
- package/dist/engine/harness/agent-harness.d.ts +2 -1
- package/dist/engine/harness/agent-harness.js +4 -1
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/llm/types.d.ts +7 -9
- package/dist/engine/loop/agent-loop.d.ts +8 -3
- package/dist/engine/loop/agent-loop.js +26 -9
- package/dist/engine/loop/types.d.ts +10 -0
- package/dist/index.d.ts +17 -5
- package/dist/index.js +14 -5
- package/dist/orchestration/run-workflow-tool.js +8 -8
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompt-assembly/assemble.d.ts +3 -1
- package/dist/prompt-assembly/assemble.js +6 -0
- package/dist/prompts/cowork.d.ts +42 -0
- package/dist/prompts/cowork.js +529 -0
- package/dist/prompts/default.d.ts +14 -1
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/session-policy-store.d.ts +6 -3
- package/dist/stores/file/session-policy-store.js +5 -16
- package/dist/tools/artifact/artifact-tool.js +2 -0
- package/dist/tools/fs/image-admission.d.ts +57 -4
- package/dist/tools/fs/image-admission.js +85 -0
- package/dist/tools/fs/path-containment.d.ts +2 -0
- package/dist/tools/fs/path-containment.js +5 -0
- package/dist/tools/fs/shell-read-check.d.ts +31 -6
- package/dist/tools/fs/shell-read-check.js +2 -4
- package/dist/tools/fs/shell-read-environment.d.ts +10 -25
- package/dist/tools/fs/shell-read-environment.js +6 -18
- package/dist/tools/fs/shell-read-establish.d.ts +3 -2
- package/dist/tools/fs/shell-read-establish.js +5 -5
- package/dist/tools/fs/shell-read-judge.d.ts +38 -8
- package/dist/tools/fs/shell-read-judge.js +12 -10
- package/dist/tools/web.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +94 -10
- package/wire-changes/core@7.51.0.json +2421 -0
package/dist/brain/openai.js
CHANGED
|
@@ -8,7 +8,7 @@ import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNot
|
|
|
8
8
|
import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders } from "./request-params.js";
|
|
9
9
|
import { BrainError, providerErrorWord } from "./errors.js";
|
|
10
10
|
import { inBandProviderError } from "./provider-dialect.js";
|
|
11
|
-
import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, createBrainRouteJudge, joinRequestUrl, resolveRequestRootOf, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
|
|
11
|
+
import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, brainConfigSnapshot, createBrainRouteJudge, joinRequestUrl, resolveRequestRootOf, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
|
|
12
12
|
import { requestRouteOf } from "./request-route.js";
|
|
13
13
|
import { declaredOffSpelling, mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
|
|
14
14
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
@@ -323,7 +323,8 @@ export function inferMaxTokensField(modelId) {
|
|
|
323
323
|
return "max_completion_tokens";
|
|
324
324
|
return "max_tokens";
|
|
325
325
|
}
|
|
326
|
-
export function createOpenAIBrain(
|
|
326
|
+
export function createOpenAIBrain(given = {}) {
|
|
327
|
+
const config = brainConfigSnapshot(given, "createOpenAIBrain config");
|
|
327
328
|
assertServableRequestRoot(config.baseUrl, "config.baseUrl");
|
|
328
329
|
refuseRetiredTimeoutKnobs(config, "construction", "createOpenAIBrain config");
|
|
329
330
|
const doFetch = resolveFetch(config);
|
|
@@ -16,7 +16,7 @@ import type { RouteCredential } from "../internal/llm.js";
|
|
|
16
16
|
* (`getApiKeyAndHeaders`), or an auth-bearing header declared on `Model.headers`. Paired by
|
|
17
17
|
* construction (the resolver was asked about THIS entry), so the adjudicator never refuses it.
|
|
18
18
|
* - `"deployment-config"` — the brain construction-time fallback (`config.apiKey` / an auth header
|
|
19
|
-
* in `config.headers`). Paired only with the deployment's own
|
|
19
|
+
* in `config.headers`). Paired only with the deployment's own root (`config.baseUrl`, else the lane's default).
|
|
20
20
|
* - `"absent"` — no credential anywhere. Legal for keyless gateways; refused only where an entry
|
|
21
21
|
* declares its OWN URL away from a declared deployment root (a distinct endpoint with no
|
|
22
22
|
* credential route is a configuration hole, not a keyless deployment). */
|
|
@@ -64,7 +64,7 @@ export interface RouteCredentialPairingRow {
|
|
|
64
64
|
readonly credentialKnobs: readonly string[];
|
|
65
65
|
/** The root knob the credential is paired with: `"Model.baseUrl"` for a per-model credential (paired by
|
|
66
66
|
* construction — it was resolved for this entry), `"config.baseUrl"` for the deployment credential (paired only
|
|
67
|
-
* when that root
|
|
67
|
+
* when the request targets that root — or, undeclared, the lane's own default endpoint). Absent for
|
|
68
68
|
* `"absent"`. */
|
|
69
69
|
readonly pairsWith?: "Model.baseUrl" | "config.baseUrl";
|
|
70
70
|
}
|
|
@@ -15,13 +15,12 @@
|
|
|
15
15
|
* - `per-model` → OK always. A per-model credential was resolved FOR this entry (auth hook or an
|
|
16
16
|
* auth header on `Model.headers`); it is paired by construction, so the per-model lane pays no
|
|
17
17
|
* new cost.
|
|
18
|
-
* - `deployment-config` →
|
|
19
|
-
*
|
|
20
|
-
* A
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* it; the posture is surfaced so read faces can annotate the unverifiable pairing instead.
|
|
18
|
+
* - `deployment-config` → a deployment credential has exactly ONE root: the one its brain sends to when
|
|
19
|
+
* an entry names none — `config.baseUrl`, else the lane's own default endpoint
|
|
20
|
+
* (`RoutePairingConfig.laneDefault`). A request to that root is `"paired"`; a request to any other
|
|
21
|
+
* root — an entry carrying its own `baseUrl` — is the torn pair this module exists to stop
|
|
22
|
+
* (`route.credential_mismatch`), whether or not the deployment wrote its root down. A key on the
|
|
23
|
+
* config with the URL only on the model is that torn pair: nothing says the key belongs there.
|
|
25
24
|
* - `absent` → OK for keyless deployments (nothing to protect); refused as
|
|
26
25
|
* `route.credential_missing` only where an entry declares its OWN URL away from a DECLARED
|
|
27
26
|
* deployment root — a distinct endpoint with no credential route is a configuration hole, and a
|
|
@@ -180,10 +179,20 @@ export declare function applyRouteCredentialHeaders(merged: Record<string, strin
|
|
|
180
179
|
* EXACTLY the resolution + judgment the brain's own request build runs (single source, two call
|
|
181
180
|
* sites), so a pre-flight verdict and the request-time verdict can never disagree.
|
|
182
181
|
*/
|
|
182
|
+
/** A brain's construction config, received ONCE by the engine's receipt law for caller objects
|
|
183
|
+
* ({@link receiveOwnProperties}: a plain object, every own member read once): the request gate and the pre-flight
|
|
184
|
+
* judge both read this frozen copy, so a config object mutated after construction moves neither and the two cannot
|
|
185
|
+
* disagree about the root a credential is paired with. The header bag is a caller object too: it is received by the
|
|
186
|
+
* same law from that one reading into a frozen bag of the copy's own. */
|
|
187
|
+
export declare function brainConfigSnapshot<T extends {
|
|
188
|
+
headers?: Record<string, string>;
|
|
189
|
+
}>(given: T, subject: string): Readonly<T>;
|
|
183
190
|
export declare function createBrainRouteJudge(config: {
|
|
184
191
|
apiKey?: string;
|
|
185
192
|
baseUrl?: string;
|
|
186
193
|
headers?: Record<string, string>;
|
|
194
|
+
/** The lane's own default endpoint (the brain's constant), when it has one. */
|
|
195
|
+
laneDefault?: string;
|
|
187
196
|
}): (model: Model, perModelAuth?: {
|
|
188
197
|
apiKey?: string;
|
|
189
198
|
headers?: Record<string, string>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AUTH_CARRIER_NAMES } from "./request-params.js";
|
|
2
2
|
import { BrainError } from "./errors.js";
|
|
3
|
+
import { receiptRefusal, receiveOwnProperties } from "../core/own-property-receipt.js";
|
|
3
4
|
const REDACTED_VALUE = "«redacted»";
|
|
4
5
|
const REDACTED_CREDENTIALS = "[redacted-credentials]";
|
|
5
6
|
function servableParse(raw) {
|
|
@@ -223,17 +224,21 @@ export function resolveRouteCredential(input) {
|
|
|
223
224
|
export function adjudicateModelRoute(model, credential, config) {
|
|
224
225
|
const entry = routeKeyOf(model.baseUrl);
|
|
225
226
|
const declared = routeKeyOf(config.baseUrl);
|
|
226
|
-
const
|
|
227
|
+
const deploymentRoot = declared || routeKeyOf(config.laneDefault);
|
|
228
|
+
const request = entry || deploymentRoot;
|
|
227
229
|
const entryShown = displayUrlOf(entry);
|
|
228
230
|
const declaredShown = displayUrlOf(declared);
|
|
229
231
|
const requestShown = displayUrlOf(request);
|
|
230
232
|
if (credential.source === "per-model")
|
|
231
233
|
return { ok: true, posture: "per-model" };
|
|
232
234
|
if (credential.source === "deployment-config") {
|
|
233
|
-
if (
|
|
234
|
-
return { ok: true, posture: "unpinned" };
|
|
235
|
-
if (request === declared)
|
|
235
|
+
if (request !== "" && request === deploymentRoot)
|
|
236
236
|
return { ok: true, posture: "paired" };
|
|
237
|
+
const pairedWith = declared !== ""
|
|
238
|
+
? `is paired with ${declaredShown}`
|
|
239
|
+
: deploymentRoot !== ""
|
|
240
|
+
? `is paired with this brain's default endpoint ${displayUrlOf(deploymentRoot)} (config.baseUrl is unset)`
|
|
241
|
+
: `names no root of its own (config.baseUrl is unset)`;
|
|
237
242
|
return {
|
|
238
243
|
ok: false,
|
|
239
244
|
code: "route.credential_mismatch",
|
|
@@ -242,8 +247,8 @@ export function adjudicateModelRoute(model, credential, config) {
|
|
|
242
247
|
entryBaseUrl: entryShown,
|
|
243
248
|
requestBaseUrl: requestShown,
|
|
244
249
|
configBaseUrl: declaredShown,
|
|
245
|
-
fixHint: `model "${model.id}" targets ${requestShown} but the deployment credential
|
|
246
|
-
`${queryDifferenceNote(request,
|
|
250
|
+
fixHint: `model "${model.id}" targets ${requestShown || "no root"} but the deployment credential ${pairedWith}` +
|
|
251
|
+
`${queryDifferenceNote(request, deploymentRoot)}: ` +
|
|
247
252
|
`give the model its own credential (a per-model key via getApiKeyAndHeaders, or an auth header on Model.headers), ` +
|
|
248
253
|
`or serve it through a brain whose config.baseUrl is the model's own root`,
|
|
249
254
|
},
|
|
@@ -285,6 +290,17 @@ export function applyRouteCredentialHeaders(merged, credential, bags) {
|
|
|
285
290
|
merged[k] = v;
|
|
286
291
|
}
|
|
287
292
|
}
|
|
293
|
+
export function brainConfigSnapshot(given, subject) {
|
|
294
|
+
const words = { subject, unit: "member", moment: "at construction" };
|
|
295
|
+
const received = receiveOwnProperties(given, words);
|
|
296
|
+
const headers = received.headers;
|
|
297
|
+
if (headers !== undefined) {
|
|
298
|
+
if (headers === null || typeof headers !== "object")
|
|
299
|
+
throw receiptRefusal(`${subject}.headers must be an object of header names to values`);
|
|
300
|
+
Object.defineProperty(received, "headers", { value: Object.freeze(receiveOwnProperties(headers, { ...words, subject: `${subject}.headers`, unit: "header" })), enumerable: true });
|
|
301
|
+
}
|
|
302
|
+
return Object.freeze(received);
|
|
303
|
+
}
|
|
288
304
|
export function createBrainRouteJudge(config) {
|
|
289
305
|
return (model, perModelAuth) => {
|
|
290
306
|
const credential = resolveRouteCredential({
|
|
@@ -294,7 +310,7 @@ export function createBrainRouteJudge(config) {
|
|
|
294
310
|
configApiKey: config.apiKey,
|
|
295
311
|
configHeaders: config.headers,
|
|
296
312
|
});
|
|
297
|
-
return adjudicateModelRoute(model, credential, { baseUrl: config.baseUrl });
|
|
313
|
+
return adjudicateModelRoute(model, credential, { baseUrl: config.baseUrl, laneDefault: config.laneDefault });
|
|
298
314
|
};
|
|
299
315
|
}
|
|
300
316
|
export function routePairingStatus(brain, model, perModelAuth) {
|
|
@@ -101,12 +101,30 @@ export const ROUTE_ADJUDICATION_CONFORMANCE_CORPUS = [
|
|
|
101
101
|
leg: "named",
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
|
-
name: "deployment-
|
|
105
|
-
intent: "
|
|
106
|
-
model: { id: "m-
|
|
107
|
-
credential: { source: "deployment-config", apiKey: "k-
|
|
104
|
+
name: "deployment-undeclared-entry-root",
|
|
105
|
+
intent: "a config key with NO declared root and an entry carrying its own root: nothing says the key belongs there — refused, never sent (declare config.baseUrl, or give the entry its own credential)",
|
|
106
|
+
model: { id: "m-catalogue", baseUrl: "https://api.example.com/v1" },
|
|
107
|
+
credential: { source: "deployment-config", apiKey: "k-deployment" },
|
|
108
108
|
config: {},
|
|
109
|
-
expect: { ok:
|
|
109
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
110
|
+
leg: "named",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "deployment-lane-default",
|
|
114
|
+
intent: "a config key with no declared root is paired with the lane's own default endpoint — a request there is paired",
|
|
115
|
+
model: { id: "m-vendor", baseUrl: "https://api.vendor.example" },
|
|
116
|
+
credential: { source: "deployment-config", apiKey: "k-vendor" },
|
|
117
|
+
config: { laneDefault: "https://api.vendor.example" },
|
|
118
|
+
expect: { ok: true, posture: "paired" },
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "deployment-lane-default-entry-elsewhere",
|
|
122
|
+
intent: "the same key with an entry rooted away from the lane default is refused, exactly like a declared root",
|
|
123
|
+
model: { id: "m-catalogue", baseUrl: "https://third-party.example.net/v1" },
|
|
124
|
+
credential: { source: "deployment-config", apiKey: "k-vendor" },
|
|
125
|
+
config: { laneDefault: "https://api.vendor.example" },
|
|
126
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
127
|
+
leg: "named",
|
|
110
128
|
},
|
|
111
129
|
{
|
|
112
130
|
name: "absent-keyless-on-root",
|
|
@@ -9,7 +9,7 @@ import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.j
|
|
|
9
9
|
import { bindBrainStatusEmitter, emitBrainStatus, emitBrainTelemetry, reportExtraBodyDropped } from "./status-sink.js";
|
|
10
10
|
import { createAttemptController, firstByteWindow, refuseRetiredTimeoutKnobs, resolveFirstByteRetries, resolveWatchdogMs, STALL_FIRST_BYTE_MS, STALL_FIRST_TOKEN_MS, STALL_IDLE_MS, } from "./timeout.js";
|
|
11
11
|
import { readMember, thrownText } from "../core/thrown-value.js";
|
|
12
|
-
import {
|
|
12
|
+
import { egressText } from "../core/untrusted-egress.js";
|
|
13
13
|
const DEFAULT_MAX_RETRIES = 10;
|
|
14
14
|
const MAX_RETRIES_ENV_CEILING = 15;
|
|
15
15
|
const MAX_RETRIES_ENV = "SEMA_MAX_RETRIES";
|
|
@@ -455,7 +455,7 @@ export function runStreamingBrain(args) {
|
|
|
455
455
|
};
|
|
456
456
|
};
|
|
457
457
|
try {
|
|
458
|
-
r = await doFetch(req.url, { method: "POST", headers: req.headers, body: req.body, signal: cc.signal });
|
|
458
|
+
r = await doFetch(req.url, { method: "POST", headers: req.headers, body: req.body, signal: cc.signal, redirect: "manual" });
|
|
459
459
|
requestIdSeen = readProviderRequestId(r);
|
|
460
460
|
if (cc.timedOut()) {
|
|
461
461
|
discardResponseBody(r);
|
|
@@ -474,7 +474,7 @@ export function runStreamingBrain(args) {
|
|
|
474
474
|
}
|
|
475
475
|
else if (classifyFetchThrow(e) === "request_unbuildable") {
|
|
476
476
|
cc.dispose();
|
|
477
|
-
throw new BrainError("invalid_request", `${httpLabel} request could not be built: ${
|
|
477
|
+
throw new BrainError("invalid_request", `${httpLabel} request could not be built: ${egressText("same-principal", stripUrlCredentials(thrownText(e)))}`);
|
|
478
478
|
}
|
|
479
479
|
else {
|
|
480
480
|
netErr = e;
|
|
@@ -486,6 +486,12 @@ export function runStreamingBrain(args) {
|
|
|
486
486
|
else
|
|
487
487
|
headerWait.stop();
|
|
488
488
|
}
|
|
489
|
+
if (r !== undefined && (r.type === "opaqueredirect" || (r.status >= 300 && r.status < 400))) {
|
|
490
|
+
const status = r.status;
|
|
491
|
+
discardResponseBody(r);
|
|
492
|
+
cc.dispose();
|
|
493
|
+
throw new BrainError("network", `${httpLabel} endpoint answered with a redirect${status > 0 ? ` (HTTP ${status})` : ""}; it was not followed and no credential was sent anywhere else — declare the final endpoint as the brain's baseUrl`);
|
|
494
|
+
}
|
|
489
495
|
if (r && r.ok && r.body) {
|
|
490
496
|
cc.clearTimer();
|
|
491
497
|
connect = cc;
|
package/dist/core/a2a.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { A2A_TASK_STATES } from "./a2a-task-state.js";
|
|
4
|
-
import { resolveProtocolHttpHeaders } from "./
|
|
4
|
+
import { assertEndpointBound, redirectPolicy, resolveProtocolHttpHeaders, underDeclaredHeaders } from "./protocol-http-auth.js";
|
|
5
5
|
import { describeHttpTransportFailure } from "./mcp-failure.js";
|
|
6
6
|
import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName } from "./protocol-naming.js";
|
|
7
7
|
import { A2A_NAMESPACE } from "./protocol-table.js";
|
|
@@ -146,12 +146,8 @@ async function requestJson(t, what, init) {
|
|
|
146
146
|
const res = await doFetch(init.url, {
|
|
147
147
|
method: init.method,
|
|
148
148
|
signal: deadline.signal,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"a2a-version": A2A_PROTOCOL_VERSION,
|
|
152
|
-
...(init.body !== undefined ? { "content-type": "application/json" } : {}),
|
|
153
|
-
...t.headers,
|
|
154
|
-
},
|
|
149
|
+
redirect: redirectPolicy(t.headers),
|
|
150
|
+
headers: underDeclaredHeaders({ accept: "application/json", "a2a-version": A2A_PROTOCOL_VERSION, ...(init.body !== undefined ? { "content-type": "application/json" } : {}) }, t.headers),
|
|
155
151
|
...(init.body !== undefined ? { body: init.body } : {}),
|
|
156
152
|
});
|
|
157
153
|
const text = await readBoundedText(res, `${what} from A2A peer "${inlineUntrusted(t.peer, A2A_ID_MAX_CHARS)}"`);
|
|
@@ -544,10 +540,12 @@ function buildSkillTool(spec, t, skill, lifecycle) {
|
|
|
544
540
|
},
|
|
545
541
|
};
|
|
546
542
|
}
|
|
547
|
-
async function materializePeer(
|
|
543
|
+
async function materializePeer(declaredSpec, principal, lifecycle, signal) {
|
|
544
|
+
const spec = Object.freeze({ ...declaredSpec });
|
|
548
545
|
const headers = resolveProtocolHttpHeaders(A2A_NAMESPACE.id, spec, principal);
|
|
549
546
|
const discovery = { peer: spec.name, endpoint: spec.url, headers };
|
|
550
547
|
const card = readCard(spec.name, await discoverCardBody(spec, discovery, signal), spec.url);
|
|
548
|
+
assertEndpointBound(A2A_NAMESPACE.id, spec.name, card.endpoint, spec.cardUrl === undefined ? [spec.url] : [spec.url, spec.cardUrl], headers);
|
|
551
549
|
const transport = { peer: spec.name, endpoint: card.endpoint, headers };
|
|
552
550
|
const tools = [];
|
|
553
551
|
const axes = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type RuleToolNameVerdict } from "./tool-name-aliases.js";
|
|
2
2
|
import type { ToolCallRequest } from "./tool-policy.js";
|
|
3
3
|
import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
|
|
4
|
-
import { type PathRuleBases, type PersistedRule } from "./permission-rule-model.js";
|
|
4
|
+
import { type AdmissionPosture, type PathRuleBases, type PersistedRule } from "./permission-rule-model.js";
|
|
5
5
|
import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
|
|
6
6
|
import { type ToolNameListReading } from "./tool-name-list.js";
|
|
7
7
|
import { type EngineNotice } from "./engine-notice.js";
|
|
@@ -10,11 +10,13 @@ import { type EngineNotice } from "./engine-notice.js";
|
|
|
10
10
|
* run mounts no tool of that exact name but mounts one that differs only by case (`currentName` is its spelling). */
|
|
11
11
|
export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "content_unsupported", "grammar", "tool_name", "near_miss"];
|
|
12
12
|
export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number];
|
|
13
|
-
/** The facts beside a reason: for `tool_name` / `near_miss` the judgment's word
|
|
14
|
-
* one
|
|
15
|
-
*
|
|
13
|
+
/** The facts beside a reason: for `tool_name` / `near_miss` the judgment's word, the judgment's own one-line sentence when it has
|
|
14
|
+
* one (`guidance`, byte for byte — `retired` and `unmintable_segment` carry one, `unprefixed_namespace` none) and the spelling to
|
|
15
|
+
* write instead when there is one; for a list member that is not a string (`grammar`) the TYPE of the value (`a number`, `null`,
|
|
16
|
+
* `an object`…) — `entry` then holds {@link nonStringMemberLabel}, never the value. */
|
|
16
17
|
export interface AllowEntryInertDetail {
|
|
17
18
|
readonly verdict?: Exclude<RuleToolNameVerdict["kind"], "current">;
|
|
19
|
+
readonly guidance?: string;
|
|
18
20
|
readonly currentName?: string;
|
|
19
21
|
readonly valueType?: string;
|
|
20
22
|
}
|
|
@@ -60,18 +62,32 @@ export type CompiledAllowEntry = {
|
|
|
60
62
|
readonly kind: "domain";
|
|
61
63
|
readonly rule: PersistedRule;
|
|
62
64
|
};
|
|
65
|
+
/** What one rule entry means under one direction ({@link readRuleEntry}): the row the grammar compiles it to, or the reason it
|
|
66
|
+
* reads as nothing with the facts a sentence about it names. */
|
|
67
|
+
export type RuleEntryReading = {
|
|
68
|
+
readonly kind: "row";
|
|
69
|
+
readonly compiled: CompiledAllowEntry;
|
|
70
|
+
} | ({
|
|
71
|
+
readonly kind: "inert";
|
|
72
|
+
readonly reason: AllowEntryInertReason;
|
|
73
|
+
} & AllowEntryInertDetail);
|
|
63
74
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* `
|
|
75
|
+
* THE ONE READER of a rule entry — a member of an entry list in either direction: a skill's `allowed-tools` /
|
|
76
|
+
* `disallowed-tools`, a sub-agent's deny list, the request's and the deployment's `allowedTools`, a session's `toolDeny` /
|
|
77
|
+
* `toolAllow`. Every station that compiles, validates, compares or names such an entry reads this answer and no other.
|
|
78
|
+
* `roster` = the tool names the reader knows are mounted (aliases included): given, the entry's tool name is also read against
|
|
79
|
+
* it (a stale name, or one that differs from a mounted tool's only by case, reads as `tool_name` / `near_miss`); absent, the
|
|
80
|
+
* grammar alone is read — what a resume's pre-claim screen can read before the leg has a roster.
|
|
67
81
|
*/
|
|
68
|
-
export declare function
|
|
82
|
+
export declare function readRuleEntry(raw: unknown, behavior: "allow" | "deny", roster?: ReadonlySet<string>): RuleEntryReading;
|
|
83
|
+
/** The tool name a compiled entry speaks for — its tool, its namespace spelling, or its rule's tool (what a name audit reads). */
|
|
84
|
+
export declare function compiledEntryName(compiled: CompiledAllowEntry): string;
|
|
69
85
|
/**
|
|
70
|
-
* Compile one list WHOLE: the entries it reads and the ones it reads as nothing (each with its
|
|
71
|
-
* roster refuses, the judgment's word and the spelling to write). An allow list holding `*` voids
|
|
72
|
-
* on it reads as nothing too: the list grants nothing — the safe direction). A `*` on a deny list
|
|
73
|
-
* and the rest of that list still refuses: a deny list never loses an entry to a wildcard beside
|
|
74
|
-
* {@link
|
|
86
|
+
* Compile one list WHOLE through {@link readRuleEntry}: the entries it reads and the ones it reads as nothing (each with its
|
|
87
|
+
* reason and, for a name the roster refuses, the judgment's word and the spelling to write). An allow list holding `*` voids
|
|
88
|
+
* the list (every other entry on it reads as nothing too: the list grants nothing — the safe direction). A `*` on a deny list
|
|
89
|
+
* reads as nothing by itself and the rest of that list still refuses: a deny list never loses an entry to a wildcard beside
|
|
90
|
+
* it. `roster` as in {@link readRuleEntry}.
|
|
75
91
|
*/
|
|
76
92
|
export declare function compileAllowList(entries: readonly string[], behavior: "allow" | "deny", roster?: ReadonlySet<string>): {
|
|
77
93
|
rows: Array<{
|
|
@@ -107,16 +123,20 @@ export interface AllowEntryJudgeContext {
|
|
|
107
123
|
readonly liveCwd: () => string | undefined;
|
|
108
124
|
readonly anchors: PathRuleBases;
|
|
109
125
|
}
|
|
126
|
+
/** For a list whose allow rows admitted no call ({@link judgeAllowEntries}): the first segment of the call's command no command
|
|
127
|
+
* row covers under `posture` — the word a refusal names; `undefined` when the call carries no command or no single segment is
|
|
128
|
+
* to blame (a line the matcher cannot read at all). The same walk the matcher took. */
|
|
129
|
+
export declare function uncoveredCommandSegment(rows: readonly AllowEntryRow[], req: ToolCallRequest, ctx: AllowEntryJudgeContext, posture: AdmissionPosture): string | undefined;
|
|
110
130
|
/**
|
|
111
131
|
* The ONE list matcher, both directions — the rows that speak for the call, or undefined. A tool-name row first (the first
|
|
112
132
|
* that reaches the call); then the content rows, each family by the predicate the stored rows of that grammar meet:
|
|
113
|
-
* COMMAND — allow: the persisted allow matcher's coverage (the whole command, or each
|
|
114
|
-
* by itself); deny: a row whose program run the call performs (the
|
|
115
|
-
* against the row is refused too). PATH — the persisted path adjudication
|
|
116
|
-
* target inside what the row names; deny: any view reaching it). NAME / DOMAIN —
|
|
117
|
-
* call names, the host it fetches).
|
|
133
|
+
* COMMAND — allow: the persisted allow matcher's coverage under the list's {@link AdmissionPosture} (the whole command, or each
|
|
134
|
+
* segment covered by a row — or, for a `grant` list, read-only by itself); deny: a row whose program run the call performs (the
|
|
135
|
+
* wide tightening reading; a call that cannot be read against the row is refused too). PATH — the persisted path adjudication
|
|
136
|
+
* (allow: the narrow reading, every view of the target inside what the row names; deny: any view reaching it). NAME / DOMAIN —
|
|
137
|
+
* the one predicate of each (the name the call names, the host it fetches).
|
|
118
138
|
*/
|
|
119
|
-
export declare function judgeAllowEntries<R extends AllowEntryRow>(rows: readonly R[], req: ToolCallRequest, behavior: "allow" | "deny", ctx: AllowEntryJudgeContext): readonly R[] | undefined;
|
|
139
|
+
export declare function judgeAllowEntries<R extends AllowEntryRow>(rows: readonly R[], req: ToolCallRequest, behavior: "allow" | "deny", ctx: AllowEntryJudgeContext, posture?: AdmissionPosture): readonly R[] | undefined;
|
|
120
140
|
/** The allow rows of a list, as the allow layer reads them in `mode`: the strip applied BEFORE the match (in `auto`). */
|
|
121
141
|
export declare function allowingRows<R extends AllowEntryRow>(rows: readonly R[], req: ToolCallRequest, mode: PermissionMode | undefined): readonly R[];
|
|
122
142
|
/** The two sources {@link AllowEntryTable} holds (a skill's list is the skill table's). */
|
|
@@ -159,7 +179,7 @@ export declare class AllowEntryTable {
|
|
|
159
179
|
private held;
|
|
160
180
|
private readonly heldEntries;
|
|
161
181
|
/** Write a list's rows (a row already held is not held twice). Returns the entries that read as nothing. `roster` as in
|
|
162
|
-
* {@link
|
|
182
|
+
* {@link readRuleEntry}: a list the leg itself wrote is read against it, a carried one (a delegating parent's capture) is not. */
|
|
163
183
|
seed(entries: readonly string[], roster?: ReadonlySet<string>): readonly AllowEntryInert[];
|
|
164
184
|
/** The entries held, as written (a copy) — what a delegated child is handed. */
|
|
165
185
|
entries(): readonly string[];
|
|
@@ -210,8 +230,9 @@ export declare function readAllowRuleShadows(value: unknown): readonly AllowRule
|
|
|
210
230
|
/** Why a matched entry did not clear the ask, by the ask's class — one row per class, so a class added later is a
|
|
211
231
|
* compile error here rather than somebody else's reason on the card. Two facts the ask carries are read first, because
|
|
212
232
|
* they name the reason more exactly than a class shared by several: a table word (`mandate` — the exact mark) and the
|
|
213
|
-
* deployment's own authorship (`authoredBy`).
|
|
214
|
-
|
|
233
|
+
* deployment's own authorship (`authoredBy`) — {@link allowRuleShadowSentence} is the reader that applies that order.
|
|
234
|
+
* Exported (frozen) so a host that renders the card's "why it cannot be cleared" half reads the engine's row, never a copy. */
|
|
235
|
+
export declare const SHADOW_REASON: Readonly<{
|
|
215
236
|
readonly content_question: "the call is the engine's own question to a person";
|
|
216
237
|
readonly hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person";
|
|
217
238
|
readonly ancestor: "the task that delegated this one owns this question";
|
|
@@ -220,8 +241,7 @@ declare const SHADOW_REASON: {
|
|
|
220
241
|
readonly unverifiable_shell_text: "the text this shell will run cannot be read by the engine — it is cleared by confirming this call, never by an allow rule";
|
|
221
242
|
readonly transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule";
|
|
222
243
|
readonly org_rule: "an organization policy rule requires approval for this call";
|
|
223
|
-
readonly
|
|
224
|
-
readonly rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown";
|
|
244
|
+
readonly rule_unreadable: "a rule that may govern this call cannot currently be read (this deployment's organization policy, a partition of the person's standing rules, or this call against a deny or ask rule), so whether one forbids it is unknown";
|
|
225
245
|
readonly ask_rule: string;
|
|
226
246
|
readonly denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides";
|
|
227
247
|
readonly classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides";
|
|
@@ -238,7 +258,7 @@ declare const SHADOW_REASON: {
|
|
|
238
258
|
readonly mode: "the permission mode puts this question to a person";
|
|
239
259
|
readonly unstated: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule";
|
|
240
260
|
readonly ordinary: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule";
|
|
241
|
-
}
|
|
261
|
+
}>;
|
|
242
262
|
/** The fence over the reason table: `never` while every class has a row. */
|
|
243
263
|
export type ShadowReasonTableCoversEveryClass = AssertAllKeysHandled<Exclude<AskClass, keyof typeof SHADOW_REASON>>;
|
|
244
264
|
/**
|
|
@@ -292,4 +312,3 @@ export declare function inertEntryPhrase(e: AllowEntryInert): string;
|
|
|
292
312
|
* `disallowed-tools` holds such an entry is named as one that does not activate.
|
|
293
313
|
*/
|
|
294
314
|
export declare function allowedToolsNotGrantedNotice(inert: readonly AllowEntryInertNote[], sessionId: string | undefined): EngineNotice;
|
|
295
|
-
export {};
|
package/dist/core/allow-entry.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermission
|
|
|
3
3
|
import { judgeRuleToolName, splitNamespacedName } from "./tool-name-aliases.js";
|
|
4
4
|
import { catalogToolNames, isShellFaceToolName } from "./tool-registry.js";
|
|
5
5
|
import { ALLOW_RULE_MEMBERS } from "./allow-rule-shadow.js";
|
|
6
|
-
import { COMMAND_RULE_TOOL, adjudicatePersistedDomainRules, adjudicatePersistedNameRules,
|
|
6
|
+
import { COMMAND_RULE_TOOL, adjudicatePersistedDomainRules, adjudicatePersistedNameRules, commandCoverageOf, domainRuleSubjectOf, laneToolNamesOf, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
|
|
7
7
|
import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
|
|
8
8
|
import { callTargetReadingOf } from "./effective-path-target.js";
|
|
9
9
|
import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
@@ -31,20 +31,21 @@ function mountedSpellingOf(name, roster) {
|
|
|
31
31
|
}
|
|
32
32
|
function toolNameReading(name, roster) {
|
|
33
33
|
const verdict = judgeRuleToolName(name, roster);
|
|
34
|
+
if (verdict.kind === "unprefixed_namespace")
|
|
35
|
+
return { kind: "inert", reason: "tool_name", verdict: verdict.kind };
|
|
34
36
|
if (verdict.kind !== "current") {
|
|
35
|
-
|
|
36
|
-
return { inert: "tool_name", detail: { verdict: verdict.kind, ...(currentName !== undefined ? { currentName } : {}) } };
|
|
37
|
+
return { kind: "inert", reason: "tool_name", verdict: verdict.kind, guidance: verdict.guidance, ...(verdict.currentName !== undefined ? { currentName: verdict.currentName } : {}) };
|
|
37
38
|
}
|
|
38
39
|
if (roster.has(name) || catalogToolNames().has(name) || splitNamespacedName(name) !== undefined)
|
|
39
40
|
return undefined;
|
|
40
41
|
const mounted = mountedSpellingOf(name, roster);
|
|
41
|
-
return mounted === undefined ? undefined : {
|
|
42
|
+
return mounted === undefined ? undefined : { kind: "inert", reason: "near_miss", currentName: mounted };
|
|
42
43
|
}
|
|
43
|
-
function
|
|
44
|
-
const inert = (reason) => ({
|
|
45
|
-
const row = (compiled) => ({ compiled });
|
|
44
|
+
export function readRuleEntry(raw, behavior, roster) {
|
|
45
|
+
const inert = (reason) => ({ kind: "inert", reason });
|
|
46
|
+
const row = (compiled) => ({ kind: "row", compiled });
|
|
46
47
|
if (typeof raw !== "string")
|
|
47
|
-
return {
|
|
48
|
+
return { kind: "inert", reason: "grammar", valueType: valueTypeLabel(raw) };
|
|
48
49
|
if (raw !== raw.trim())
|
|
49
50
|
return inert("grammar");
|
|
50
51
|
const parsed = parsePermissionRule(raw);
|
|
@@ -82,26 +83,39 @@ function readAllowEntry(raw, behavior, roster) {
|
|
|
82
83
|
}
|
|
83
84
|
return inert(inertReasonOf(read.reject.code));
|
|
84
85
|
}
|
|
86
|
+
export function compiledEntryName(compiled) {
|
|
87
|
+
switch (compiled.kind) {
|
|
88
|
+
case "tool":
|
|
89
|
+
case "shell":
|
|
90
|
+
return compiled.name;
|
|
91
|
+
case "namespace":
|
|
92
|
+
return compiled.pattern;
|
|
93
|
+
default:
|
|
94
|
+
return compiled.rule.tool;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
85
97
|
function inertReasonOf(code) {
|
|
86
98
|
return code === "unsupported.tool" ? "content_unsupported" : "grammar";
|
|
87
99
|
}
|
|
88
|
-
export function compileAllowEntry(raw, behavior, roster) {
|
|
89
|
-
const read = readAllowEntry(raw, behavior, roster);
|
|
90
|
-
return "compiled" in read ? read.compiled : read.inert;
|
|
91
|
-
}
|
|
92
100
|
export function compileAllowList(entries, behavior, roster) {
|
|
93
|
-
const voided = behavior === "allow" && entries.some((e) =>
|
|
101
|
+
const voided = behavior === "allow" && entries.some((e) => isWildcardAll(readRuleEntry(e, behavior)));
|
|
94
102
|
const rows = [];
|
|
95
103
|
const inert = [];
|
|
96
104
|
for (const entry of entries) {
|
|
97
|
-
const read = typeof entry !== "string" || !voided ?
|
|
98
|
-
if ("
|
|
105
|
+
const read = typeof entry !== "string" || !voided ? readRuleEntry(entry, behavior, roster) : { kind: "inert", reason: "wildcard_all" };
|
|
106
|
+
if (read.kind === "row")
|
|
99
107
|
rows.push({ entry, compiled: read.compiled });
|
|
100
|
-
else
|
|
101
|
-
|
|
108
|
+
else {
|
|
109
|
+
const { kind: _kind, ...why } = read;
|
|
110
|
+
void _kind;
|
|
111
|
+
inert.push({ entry: typeof entry === "string" ? entry : nonStringMemberLabel(entry), ...why });
|
|
112
|
+
}
|
|
102
113
|
}
|
|
103
114
|
return { rows, inert };
|
|
104
115
|
}
|
|
116
|
+
function isWildcardAll(read) {
|
|
117
|
+
return read.kind === "inert" && read.reason === "wildcard_all";
|
|
118
|
+
}
|
|
105
119
|
function answersTo(req, has) {
|
|
106
120
|
return heldBy("allowEntry.names", { has }, req);
|
|
107
121
|
}
|
|
@@ -164,7 +178,18 @@ function commandOf(req) {
|
|
|
164
178
|
const command = req.args?.command;
|
|
165
179
|
return tool !== undefined && typeof command === "string" ? { tool, command } : undefined;
|
|
166
180
|
}
|
|
167
|
-
|
|
181
|
+
function admissionCallOf(call, ctx, liveCwd, posture) {
|
|
182
|
+
return { tool: call.tool, command: call.command, cwd: ctx.root, ...(ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : {}), ...(liveCwd !== undefined ? { execCwd: liveCwd } : {}), posture };
|
|
183
|
+
}
|
|
184
|
+
export function uncoveredCommandSegment(rows, req, ctx, posture) {
|
|
185
|
+
const call = commandOf(req);
|
|
186
|
+
if (call === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
const rules = rows.flatMap((r) => (r.behavior === "allow" && r.compiled.kind === "command" ? [r.compiled.rule] : []));
|
|
189
|
+
const coverage = commandCoverageOf(rules, admissionCallOf(call, ctx, ctx.liveCwd(), posture));
|
|
190
|
+
return "uncovered" in coverage ? coverage.uncovered : undefined;
|
|
191
|
+
}
|
|
192
|
+
export function judgeAllowEntries(rows, req, behavior, ctx, posture = "grant") {
|
|
168
193
|
const live = rows.filter((r) => r.behavior === behavior);
|
|
169
194
|
if (live.length === 0)
|
|
170
195
|
return undefined;
|
|
@@ -179,8 +204,8 @@ export function judgeAllowEntries(rows, req, behavior, ctx) {
|
|
|
179
204
|
if (r.compiled.kind === "command")
|
|
180
205
|
byRule.set(r.compiled.rule, r);
|
|
181
206
|
if (behavior === "allow") {
|
|
182
|
-
const
|
|
183
|
-
const hits = (covering
|
|
207
|
+
const coverage = commandCoverageOf([...byRule.keys()], admissionCallOf(call, ctx, liveCwd, posture));
|
|
208
|
+
const hits = ("covering" in coverage ? coverage.covering : []).flatMap((rule) => byRule.get(rule) ?? []);
|
|
184
209
|
if (hits.length > 0)
|
|
185
210
|
return hits;
|
|
186
211
|
}
|
|
@@ -319,7 +344,7 @@ export function readAllowRuleShadows(value) {
|
|
|
319
344
|
return undefined;
|
|
320
345
|
}
|
|
321
346
|
}
|
|
322
|
-
const SHADOW_REASON = {
|
|
347
|
+
export const SHADOW_REASON = Object.freeze({
|
|
323
348
|
content_question: "the call is the engine's own question to a person",
|
|
324
349
|
hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person",
|
|
325
350
|
ancestor: "the task that delegated this one owns this question",
|
|
@@ -328,8 +353,7 @@ const SHADOW_REASON = {
|
|
|
328
353
|
unverifiable_shell_text: "the text this shell will run cannot be read by the engine — it is cleared by confirming this call, never by an allow rule",
|
|
329
354
|
transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule",
|
|
330
355
|
org_rule: "an organization policy rule requires approval for this call",
|
|
331
|
-
|
|
332
|
-
rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown",
|
|
356
|
+
rule_unreadable: "a rule that may govern this call cannot currently be read (this deployment's organization policy, a partition of the person's standing rules, or this call against a deny or ask rule), so whether one forbids it is unknown",
|
|
333
357
|
ask_rule: mandateDisclosureOf(undefined),
|
|
334
358
|
denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides",
|
|
335
359
|
classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides",
|
|
@@ -346,7 +370,7 @@ const SHADOW_REASON = {
|
|
|
346
370
|
mode: "the permission mode puts this question to a person",
|
|
347
371
|
unstated: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
|
|
348
372
|
ordinary: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
|
|
349
|
-
};
|
|
373
|
+
});
|
|
350
374
|
function memberPhrase(s) {
|
|
351
375
|
const listed = `${s.entries.join(", ")}${s.omitted !== undefined ? ` and ${s.omitted} more` : ""}`;
|
|
352
376
|
switch (s.member) {
|