From b1c0a1aa05b39250c025abfe7142731651c162f5 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 16:06:50 -0700 Subject: [PATCH 01/11] feat(enclaves): add trusted response schemas Add an unmetered trusted sensitivity that permits free-form string nodes inside otherwise strict structured enclave responses. Enforce trusted-only use before execution and retain existing result-size and schema constraints. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- .../bounded-execution/finite-disclosure.js | 33 +++++++++- .../bounded-execution/sensitivity-policy.js | 12 ++-- containers/enclave/agent-entrypoint.py | 2 +- containers/enclave/mcp-server/mcp-protocol.js | 7 ++- .../script-executor/executor-handler.js | 10 ++- docs/awf-config-spec.md | 19 ++++++ docs/awf-config.schema.json | 1 + src/awf-config-schema.json | 1 + .../finite-disclosure-string.test.ts | 62 +++++++++++++++++++ src/bounded-execution/finite-disclosure.ts | 49 +++++++++++++-- src/enclave/gateway.ts | 7 ++- src/enclave/information-budget.test.ts | 9 +++ src/enclave/mcp-server.test.ts | 46 ++++++++++++++ src/schema.test.ts | 10 +++ src/types/enclave-options.ts | 4 +- 15 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 src/bounded-execution/finite-disclosure-string.test.ts diff --git a/containers/bounded-execution/finite-disclosure.js b/containers/bounded-execution/finite-disclosure.js index b665b549f..f53d847f4 100644 --- a/containers/bounded-execution/finite-disclosure.js +++ b/containers/bounded-execution/finite-disclosure.js @@ -118,6 +118,12 @@ function buildSchemaNode(raw, ctx, depth) { } return { type: 'boolean' }; } + case 'string': { + if (Object.keys(node).length !== 1) { + return failSchema(ctx, 'string schema must have only "type"'); + } + return { type: 'string' }; + } case 'enum': { if (Object.keys(node).length !== 2 || !('values' in node)) { return failSchema(ctx, 'enum schema must have exactly "type" and "values"'); @@ -250,7 +256,7 @@ function buildSchemaNode(raw, ctx, depth) { default: return failSchema( ctx, - 'schema node "type" must be one of: const, boolean, enum, integer, object, tuple, array, union', + 'schema node "type" must be one of: const, boolean, string, enum, integer, object, tuple, array, union', ); } } @@ -291,6 +297,8 @@ function schemaCardinality(schema) { return 1n; case 'boolean': return 2n; + case 'string': + throw new Error('free-form string schemas do not have finite cardinality'); case 'enum': return BigInt(schema.values.length); case 'integer': @@ -336,6 +344,8 @@ function cappedSchemaCardinality(schema) { return 1n; case 'boolean': return 2n; + case 'string': + throw new Error('free-form string schemas do not have finite cardinality'); case 'enum': return BigInt(schema.values.length); case 'integer': @@ -381,6 +391,8 @@ function validateValueAgainstSchema(schema, value) { return jsonLiteralEquals(value, schema.value); case 'boolean': return typeof value === 'boolean'; + case 'string': + return typeof value === 'string'; case 'enum': return schema.values.some((candidate) => jsonLiteralEquals(value, candidate)); case 'integer': @@ -429,6 +441,7 @@ function canonicalizeSchemaValue(schema, value) { case 'const': return JSON.stringify(schema.value); case 'boolean': + case 'string': case 'enum': case 'integer': return JSON.stringify(value); @@ -452,6 +465,23 @@ function canonicalizeSchemaValue(schema, value) { } } +function schemaContainsFreeformString(schema) { + switch (schema.type) { + case 'string': + return true; + case 'object': + return schema.fields.some((field) => schemaContainsFreeformString(field.schema)); + case 'tuple': + return schema.items.some(schemaContainsFreeformString); + case 'array': + return schemaContainsFreeformString(schema.items); + case 'union': + return schema.variants.some((variant) => schemaContainsFreeformString(variant.schema)); + default: + return false; + } +} + // ── Strict JSON parsing (no `JSON.parse`) ──────────────────────────────────── const MAX_JSON_PARSE_DEPTH = 32; @@ -683,6 +713,7 @@ module.exports = { informationChargeForSchema, validateValueAgainstSchema, canonicalizeSchemaValue, + schemaContainsFreeformString, strictParseJson, validateEnclaveScriptRequest, canonicalSuccessJson, diff --git a/containers/bounded-execution/sensitivity-policy.js b/containers/bounded-execution/sensitivity-policy.js index 39d294da5..1c409ef41 100644 --- a/containers/bounded-execution/sensitivity-policy.js +++ b/containers/bounded-execution/sensitivity-policy.js @@ -5,18 +5,20 @@ * budgets — server-side mirror of `ENCLAVE_SENSITIVITY_RUN_BITS` in * `src/types/enclave-options.ts`. * - * `null` means "unmetered": `public` still runs through the same finite - * schema/result validation and operational limits (`maxInvocations`, - * timeouts, sandboxing) as every other category, but its responses are not - * debited against a confidentiality ledger. `sealed` is `0`, which — + * `null` means "unmetered": `public` still requires finite schemas, while + * `trusted` may additionally use free-form string schema nodes. Both retain + * schema/result validation and operational limits (`maxInvocations`, timeouts, + * sandboxing), but their responses are not debited against a confidentiality + * ledger. `sealed` is `0`, which — * because every accepted query's minimum charge is 5 bits (1 status bit + * 4 timing bits) — always exceeds the remaining balance, so a `sealed` * repository can never fund a single query and therefore never copies a * seed or launches Python. */ -const ENCLAVE_SENSITIVITIES = ['public', 'internal', 'confidential', 'sealed']; +const ENCLAVE_SENSITIVITIES = ['trusted', 'public', 'internal', 'confidential', 'sealed']; const ENCLAVE_SENSITIVITY_RUN_BITS = { + trusted: null, public: null, internal: 64, confidential: 8, diff --git a/containers/enclave/agent-entrypoint.py b/containers/enclave/agent-entrypoint.py index 153d1e41a..2c6d16775 100644 --- a/containers/enclave/agent-entrypoint.py +++ b/containers/enclave/agent-entrypoint.py @@ -358,7 +358,7 @@ def build_prompt(task: str, schema_text: str) -> str: else: output_contract = ( "Before finishing, use a shell command to replace /awf/out with exactly one JSON " - "value conforming to this finite schema. Do not write a Markdown fence, " + "value conforming to this structured response schema. Do not write a Markdown fence, " "explanation, surrounding text, or repeated schema to /awf/out. Your " f"conversational response is not the result channel:\n{schema_text}\n" ) diff --git a/containers/enclave/mcp-server/mcp-protocol.js b/containers/enclave/mcp-server/mcp-protocol.js index 45bf8635f..349af67a4 100644 --- a/containers/enclave/mcp-server/mcp-protocol.js +++ b/containers/enclave/mcp-server/mcp-protocol.js @@ -20,12 +20,13 @@ function canonicalToolError() { const FINITE_SCHEMA_INPUT = Object.freeze({ type: 'object', - description: 'An AWF finite-disclosure schema (const, boolean, enum, integer, object, tuple, array, or union).', + description: + 'An AWF structured response schema (const, boolean, string for trusted repositories, enum, integer, object, tuple, array, or union).', }); const TOOL = Object.freeze({ name: TOOL_NAME, - description: 'Run a bounded script against one configured private repository and return one finite value.', + description: 'Run a bounded script against one configured repository and return one structured value.', inputSchema: Object.freeze({ type: 'object', properties: Object.freeze({ @@ -62,7 +63,7 @@ const AGENT_TOOL = Object.freeze({ name: AGENT_TOOL_NAME, description: 'Run a bounded, single-use agent enclave against one configured private repository and return ' - + 'one finite value.', + + 'one structured value.', inputSchema: Object.freeze({ type: 'object', properties: Object.freeze({ diff --git a/containers/enclave/script-executor/executor-handler.js b/containers/enclave/script-executor/executor-handler.js index 44b1b231f..574e0411c 100644 --- a/containers/enclave/script-executor/executor-handler.js +++ b/containers/enclave/script-executor/executor-handler.js @@ -6,6 +6,7 @@ const { canonicalSuccessJson, parseAndValidateFiniteOutput, informationChargeForSchema, + schemaContainsFreeformString, validateEnclaveScriptRequest, } = require('../../bounded-execution/finite-disclosure'); const { createEnclaveInformationBudgetLedger } = require('../../bounded-execution/sensitivity-ledger'); @@ -133,12 +134,19 @@ function createExecutorHandler(params) { await rejectBeforeExecution('repo-not-allowed', privateRepo); return; } + if (schemaContainsFreeformString(schema) && seed.sensitivity !== 'trusted') { + await rejectBeforeExecution( + 'trusted-schema-required', + `repo=${privateRepo} sensitivity=${seed.sensitivity}`, + ); + return; + } // Compute and debit the charge for THIS invocation's schema *before* // copying a seed or launching Python. Every invocation may declare a // different schema; there is no separate per-query cap — only whether // this charge fits the repository's remaining run balance. - const charge = informationChargeForSchema(schema); + const charge = seed.sensitivity === 'trusted' ? 0 : informationChargeForSchema(schema); if (!ledger.tryDebit(repoKey, charge, executorKind)) { await rejectBeforeExecution('bit-budget-exhausted', `repo=${privateRepo} charge=${charge}`); return; diff --git a/docs/awf-config-spec.md b/docs/awf-config-spec.md index 91a1587b2..c70e57a7b 100644 --- a/docs/awf-config-spec.md +++ b/docs/awf-config-spec.md @@ -1914,6 +1914,25 @@ in force. Script and agent calls debit the same live per-repository balance and share one AWF-owned admission lane. Switching executor kinds never resets or forks the ledger. +Repository sensitivity selects the response schema and per-run disclosure policy: + +| Sensitivity | Per-run budget | Response schema | +|-------------|----------------|-----------------| +| `trusted` | Unmetered | Structured schemas, including free-form `string` nodes | +| `public` | Unmetered | Finite schemas only | +| `internal` | 64 bits | Finite schemas only | +| `confidential` | 8 bits | Finite schemas only | +| `sealed` | 0 bits | No invocation can be admitted | + +The `trusted` class is intended only for repositories whose content may be +returned to the primary agent without confidentiality accounting. It permits +an exact `{ "type": "string" }` schema node at any otherwise valid schema +position. Strings remain bounded by `maxOutputBytes` and the global 8192-byte +result ceiling. The schema remains strict and structured: floats, optional +fields, extra properties, `$ref`, recursion, regex schemas, and untagged unions +are unsupported. Every other sensitivity rejects a schema containing a +free-form `string` node before launching an enclave. + `enclave_run_agent` necessarily sends repository-derived content to the configured model provider through the dedicated API proxy. The ledger bounds what the **calling agent** learns; it does not bound what the **provider** sees. ### 14.5 Validation coverage diff --git a/docs/awf-config.schema.json b/docs/awf-config.schema.json index 792ded177..eb7dea512 100644 --- a/docs/awf-config.schema.json +++ b/docs/awf-config.schema.json @@ -1160,6 +1160,7 @@ "sensitivity": { "type": "string", "enum": [ + "trusted", "public", "internal", "confidential", diff --git a/src/awf-config-schema.json b/src/awf-config-schema.json index 792ded177..eb7dea512 100644 --- a/src/awf-config-schema.json +++ b/src/awf-config-schema.json @@ -1160,6 +1160,7 @@ "sensitivity": { "type": "string", "enum": [ + "trusted", "public", "internal", "confidential", diff --git a/src/bounded-execution/finite-disclosure-string.test.ts b/src/bounded-execution/finite-disclosure-string.test.ts new file mode 100644 index 000000000..7d652a063 --- /dev/null +++ b/src/bounded-execution/finite-disclosure-string.test.ts @@ -0,0 +1,62 @@ +import * as path from 'path'; +import { + canonicalizeSchemaValue, + parseAndValidateFiniteOutput, + schemaCardinality, + schemaContainsFreeformString, + validateSchema, + validateValueAgainstSchema, +} from './finite-disclosure'; + +/* eslint-disable @typescript-eslint/no-require-imports */ +const containerProtocol = require( + path.join(__dirname, '..', '..', 'containers', 'bounded-execution', 'finite-disclosure.js'), +); +/* eslint-enable @typescript-eslint/no-require-imports */ + +const structuredStringSchema = { + type: 'object', + fields: { + action: { type: 'enum', values: ['dispatch', 'ignore'] }, + rationale: { type: 'string' }, + }, +}; + +describe.each([ + ['host', { + canonicalizeSchemaValue, + parseAndValidateFiniteOutput, + schemaCardinality, + schemaContainsFreeformString, + validateSchema, + validateValueAgainstSchema, + }], + ['container', containerProtocol], +])('trusted string schema parity: %s', (_name, protocol) => { + it('parses, detects, validates, and canonicalizes a structured free-form string', () => { + const validation = protocol.validateSchema(structuredStringSchema); + expect(validation.valid).toBe(true); + if (!validation.valid) return; + + const value = { rationale: 'Issue body can be summarized freely.', action: 'dispatch' }; + expect(protocol.schemaContainsFreeformString(validation.schema)).toBe(true); + expect(protocol.validateValueAgainstSchema(validation.schema, value)).toBe(true); + expect(protocol.canonicalizeSchemaValue(validation.schema, value)).toBe( + '{"action":"dispatch","rationale":"Issue body can be summarized freely."}', + ); + expect(protocol.parseAndValidateFiniteOutput(JSON.stringify(value), validation.schema)).toEqual({ + ok: true, + canonical: '{"action":"dispatch","rationale":"Issue body can be summarized freely."}', + }); + }); + + it('keeps string schemas exact and outside finite-cardinality accounting', () => { + expect(protocol.validateSchema({ type: 'string', maxLength: 10 }).valid).toBe(false); + const validation = protocol.validateSchema({ type: 'string' }); + expect(validation.valid).toBe(true); + if (!validation.valid) return; + expect(() => protocol.schemaCardinality(validation.schema)).toThrow( + 'free-form string schemas do not have finite cardinality', + ); + }); +}); diff --git a/src/bounded-execution/finite-disclosure.ts b/src/bounded-execution/finite-disclosure.ts index f1d12bfc3..8302aa87f 100644 --- a/src/bounded-execution/finite-disclosure.ts +++ b/src/bounded-execution/finite-disclosure.ts @@ -161,6 +161,9 @@ export interface ConstSchemaNode { export interface BooleanSchemaNode { readonly type: 'boolean'; } +export interface StringSchemaNode { + readonly type: 'string'; +} export interface EnumSchemaNode { readonly type: 'enum'; readonly values: readonly JsonLiteral[]; @@ -199,6 +202,7 @@ export interface UnionSchemaNode { export type FiniteSchemaNode = | ConstSchemaNode | BooleanSchemaNode + | StringSchemaNode | EnumSchemaNode | IntegerSchemaNode | ObjectSchemaNode @@ -269,6 +273,12 @@ function buildSchemaNode(raw: unknown, ctx: SchemaParseContext, depth: number): } return { type: 'boolean' }; } + case 'string': { + if (Object.keys(node).length !== 1) { + return failSchema(ctx, 'string schema must have only "type"'); + } + return { type: 'string' }; + } case 'enum': { if (Object.keys(node).length !== 2 || !('values' in node)) { return failSchema(ctx, 'enum schema must have exactly "type" and "values"'); @@ -402,7 +412,7 @@ function buildSchemaNode(raw: unknown, ctx: SchemaParseContext, depth: number): default: return failSchema( ctx, - 'schema node "type" must be one of: const, boolean, enum, integer, object, tuple, array, union', + 'schema node "type" must be one of: const, boolean, string, enum, integer, object, tuple, array, union', ); } } @@ -410,11 +420,13 @@ function buildSchemaNode(raw: unknown, ctx: SchemaParseContext, depth: number): /** * Validates and parses an agent-authored schema. * - * Rejects anything outside the finite algebra above: unbounded strings, - * floats, regex domains, recursion/`$ref` (there is no such construct to - * begin with), optional properties, `additionalProperties`, and overlapping - * untagged unions are all structurally impossible to express, so they are - * rejected by construction rather than by a separate deny-list. + * Rejects anything outside the structured algebra above: floats, regex + * domains, recursion/`$ref` (there is no such construct to begin with), + * optional properties, `additionalProperties`, and overlapping untagged + * unions are all structurally impossible to express, so they are rejected by + * construction rather than by a separate deny-list. Free-form string nodes + * are parsed here and authorized against trusted repository metadata by the + * enclave broker before execution. */ export function validateSchema(raw: unknown): FiniteSchemaValidation { let serialized: string; @@ -458,6 +470,8 @@ export function schemaCardinality(schema: FiniteSchemaNode): bigint { return 1n; case 'boolean': return 2n; + case 'string': + throw new Error('free-form string schemas do not have finite cardinality'); case 'enum': return BigInt(schema.values.length); case 'integer': @@ -510,6 +524,8 @@ function cappedSchemaCardinality(schema: FiniteSchemaNode): bigint { return 1n; case 'boolean': return 2n; + case 'string': + throw new Error('free-form string schemas do not have finite cardinality'); case 'enum': return BigInt(schema.values.length); case 'integer': @@ -571,6 +587,8 @@ export function validateValueAgainstSchema(schema: FiniteSchemaNode, value: unkn return jsonLiteralEquals(value, schema.value); case 'boolean': return typeof value === 'boolean'; + case 'string': + return typeof value === 'string'; case 'enum': return schema.values.some((candidate) => jsonLiteralEquals(value, candidate)); case 'integer': @@ -627,6 +645,7 @@ export function canonicalizeSchemaValue(schema: FiniteSchemaNode, value: unknown case 'const': return JSON.stringify(schema.value); case 'boolean': + case 'string': case 'enum': case 'integer': return JSON.stringify(value); @@ -655,6 +674,24 @@ export function canonicalizeSchemaValue(schema: FiniteSchemaNode, value: unknown } } +/** Whether a schema contains a free-form string node reserved for trusted repositories. */ +export function schemaContainsFreeformString(schema: FiniteSchemaNode): boolean { + switch (schema.type) { + case 'string': + return true; + case 'object': + return schema.fields.some(field => schemaContainsFreeformString(field.schema)); + case 'tuple': + return schema.items.some(schemaContainsFreeformString); + case 'array': + return schemaContainsFreeformString(schema.items); + case 'union': + return schema.variants.some(variant => schemaContainsFreeformString(variant.schema)); + default: + return false; + } +} + // ── Strict JSON parsing (no `JSON.parse`) ──────────────────────────────────── /** Hard cap on parser recursion, independent of any schema's own depth bound. */ diff --git a/src/enclave/gateway.ts b/src/enclave/gateway.ts index 6b4286bf0..bec2e0ed5 100644 --- a/src/enclave/gateway.ts +++ b/src/enclave/gateway.ts @@ -74,7 +74,8 @@ class GatewayReadinessError extends Error { const finiteSchemaInput = { type: 'object', - description: 'An AWF finite-disclosure schema (const, boolean, enum, integer, object, tuple, array, or union).', + description: + 'An AWF structured response schema (const, boolean, string for trusted repositories, enum, integer, object, tuple, array, or union).', }; const outputSchema = { @@ -89,7 +90,7 @@ const outputSchema = { const scriptTool = { name: 'enclave_run_script', - description: 'Run a bounded script against one configured private repository and return one finite value.', + description: 'Run a bounded script against one configured repository and return one structured value.', inputSchema: { type: 'object', properties: { @@ -106,7 +107,7 @@ const scriptTool = { const agentTool = { name: 'enclave_run_agent', description: - 'Run a bounded, single-use agent enclave against one configured private repository and return one finite value.', + 'Run a bounded, single-use agent enclave against one configured repository and return one structured value.', inputSchema: { type: 'object', properties: { diff --git a/src/enclave/information-budget.test.ts b/src/enclave/information-budget.test.ts index 8f43ae714..fa8c13966 100644 --- a/src/enclave/information-budget.test.ts +++ b/src/enclave/information-budget.test.ts @@ -32,4 +32,13 @@ describe('enclave information budget', () => { expect(ledger.remainingBits('OCTO/PRIVATE')).toBe(0); expect(ledger.tryDebit('octo/private', 1, 'script')).toBe(false); }); + + it('keeps trusted repositories unmetered', () => { + const ledger = createEnclaveInformationBudgetLedger(new Map([ + ['octo/trusted', { sensitivity: 'trusted' as const }], + ])); + + expect(ledger.tryDebit('octo/trusted', Number.MAX_SAFE_INTEGER, 'agent')).toBe(true); + expect(ledger.remainingBits('octo/trusted')).toBeNull(); + }); }); diff --git a/src/enclave/mcp-server.test.ts b/src/enclave/mcp-server.test.ts index d704d9082..2011aba00 100644 --- a/src/enclave/mcp-server.test.ts +++ b/src/enclave/mcp-server.test.ts @@ -334,6 +334,52 @@ describe('unified enclave ledger and timing', () => { expect(now).toBe(100); }); + it('allows free-form string fields only for trusted repositories', async () => { + const schema = { + type: 'object', + fields: { + disposition: { type: 'enum', values: ['dispatch', 'ignore'] }, + rationale: { type: 'string' }, + }, + }; + const request = { ...validArguments, schema }; + const run = jest.fn(async () => ({ exitCode: 0, timedOut: false })); + const workspace = { + createInvocationWorkspace: () => ({ outPath: 'unused' }), + readQueryOutput: () => '{"disposition":"dispatch","rationale":"Free-form trusted rationale."}', + destroyInvocationWorkspace: () => undefined, + }; + const createBroker = (sensitivity: 'trusted' | 'public') => createExecutorHandler({ + config: { + maxInvocations: 1, + timeoutSeconds: 30, + primaryBackend: 'docker', + executorBackend: 'docker', + maxOutputBytes: 8192, + }, + seedMap: new Map([['octo/private', { seedId: 'a'.repeat(16), sensitivity }]]), + runId: 'a'.repeat(16), + audit: { failure: jest.fn(), invocation: jest.fn() }, + telemetry: { emit: jest.fn() }, + executorKind: 'script', + clock: { nowMs: () => 0, sleep: async () => undefined }, + runner: { runScriptContainer: run }, + workspace, + }); + + let trustedResult = ''; + await createBroker('trusted').handle(request, (value: string) => { trustedResult = value; }); + expect(trustedResult).toBe( + '{"status":"ok","result":{"disposition":"dispatch","rationale":"Free-form trusted rationale."}}', + ); + expect(run).toHaveBeenCalledTimes(1); + + let publicResult = ''; + await createBroker('public').handle(request, (value: string) => { publicResult = value; }); + expect(publicResult).toBe(CANONICAL_ERROR_RESPONSE_JSON); + expect(run).toHaveBeenCalledTimes(1); + }); + it('buckets repository and budget rejection classes to the same public boundary', async () => { async function rejected(seedMap: Map, debit: boolean) { let now = 0; diff --git a/src/schema.test.ts b/src/schema.test.ts index 99a6a498c..0732b6db8 100644 --- a/src/schema.test.ts +++ b/src/schema.test.ts @@ -54,6 +54,16 @@ describe('awf-config.schema.json', () => { expect(validate.errors).toBeNull(); }); + it('accepts trusted enclave repository sensitivity', () => { + expect(validate({ + enclaves: [{ + script: {}, + repos: [{ repo: 'github/gh-aw', sensitivity: 'trusted' }], + }], + })).toBe(true); + expect(validate.errors).toBeNull(); + }); + it('accepts a full valid config', () => { const valid = { $schema: 'https://github.com/github/gh-aw-firewall/releases/latest/download/awf-config.schema.json', diff --git a/src/types/enclave-options.ts b/src/types/enclave-options.ts index bfb8d90b1..2f70f6854 100644 --- a/src/types/enclave-options.ts +++ b/src/types/enclave-options.ts @@ -6,9 +6,10 @@ * accepted from an invocation request. */ -export type EnclaveSensitivity = 'public' | 'internal' | 'confidential' | 'sealed'; +export type EnclaveSensitivity = 'trusted' | 'public' | 'internal' | 'confidential' | 'sealed'; export const ENCLAVE_SENSITIVITIES: readonly EnclaveSensitivity[] = [ + 'trusted', 'public', 'internal', 'confidential', @@ -17,6 +18,7 @@ export const ENCLAVE_SENSITIVITIES: readonly EnclaveSensitivity[] = [ /** Shared per-repository budget for every executor in one AWF run. */ export const ENCLAVE_SENSITIVITY_RUN_BITS: Readonly> = { + trusted: null, public: null, internal: 64, confidential: 8, From be94c044d36910e0d501826a880d4ae32d713b68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Sep 2026 23:22:01 +0000 Subject: [PATCH 02/11] fix: align enclave agent tool description --- containers/enclave/mcp-server/mcp-protocol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/containers/enclave/mcp-server/mcp-protocol.js b/containers/enclave/mcp-server/mcp-protocol.js index 349af67a4..a5eb73657 100644 --- a/containers/enclave/mcp-server/mcp-protocol.js +++ b/containers/enclave/mcp-server/mcp-protocol.js @@ -62,7 +62,7 @@ const TOOL = Object.freeze({ const AGENT_TOOL = Object.freeze({ name: AGENT_TOOL_NAME, description: - 'Run a bounded, single-use agent enclave against one configured private repository and return ' + 'Run a bounded, single-use agent enclave against one configured repository and return ' + 'one structured value.', inputSchema: Object.freeze({ type: 'object', From fb519e7a4add1a328bbac7768ddd37f530cb08a6 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 16:57:46 -0700 Subject: [PATCH 03/11] test(enclaves): isolate gh preflight lookup Keep the enclave entrypoint harness independent of executables preinstalled on the CI runner. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- src/enclave/agent-entrypoint-diagnostics.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/enclave/agent-entrypoint-diagnostics.test.ts b/src/enclave/agent-entrypoint-diagnostics.test.ts index 268d9a91d..9a5a49e4c 100644 --- a/src/enclave/agent-entrypoint-diagnostics.test.ts +++ b/src/enclave/agent-entrypoint-diagnostics.test.ts @@ -37,6 +37,12 @@ module.SHARED_MEMORY_DIR = root / "shm" module.COPILOT_BIN = str(root / "copilot") module.GITHUB_AGENT_ID_PATH = root / "github-agent-id" module.GITHUB_MCP_CONFIG_PATH = module.AGENT_DIR / "github-mcp.json" +original_which = module.shutil.which +module.shutil.which = lambda executable: ( + str(root / "gh") if executable == "gh" and scenario == "unexpected-gh" + else None if executable == "gh" + else original_which(executable) +) if scenario == "github-config": module.AGENT_DIR.mkdir() From b59e7f85f1664a3b0c8d77c8b53b9f7e044b8cd8 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 18:15:18 -0700 Subject: [PATCH 04/11] test(ci): stabilize npm package installation Use an isolated cache and disable npm metadata requests. This keeps the chroot test focused on package installation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- tests/integration/chroot-package-managers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/chroot-package-managers.test.ts b/tests/integration/chroot-package-managers.test.ts index 7969684b5..2947434f0 100644 --- a/tests/integration/chroot-package-managers.test.ts +++ b/tests/integration/chroot-package-managers.test.ts @@ -374,7 +374,7 @@ describe('Chroot Package Manager Support', () => { test('should install an npm package and verify require', async () => { const result = await runner.run( 'NPMDIR=$(mktemp -d) && cd $NPMDIR && npm init -y 2>&1 && ' + - 'npm install chalk@4 2>&1 && ' + + 'npm install --cache "$NPMDIR/.npm-cache" --no-audit --no-fund --no-update-notifier chalk@4 2>&1 && ' + 'NODE_PATH=$NPMDIR/node_modules node -e "require(\'chalk\')" && echo "npm_install_ok" && ' + 'rm -rf $NPMDIR', { From 690af42cef76fe0078a940eb8d342fd178e19745 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 18:25:32 -0700 Subject: [PATCH 05/11] ci: reuse npm audit results Enforce vulnerability thresholds from the JSON used for SARIF. Avoid a second request to the intermittently failing npm mirror. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- .github/workflows/dependency-audit.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 19e8cdc26..ac5fbac3c 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -50,8 +50,13 @@ jobs: sarif_file: npm-audit-main.sarif category: npm-audit-main - - name: Run npm audit (fail on high/critical) - run: npm audit --audit-level=high + - name: Enforce npm audit (fail on high/critical) + run: | + jq -e ' + (.error | not) and + (.metadata.vulnerabilities.high == 0) and + (.metadata.vulnerabilities.critical == 0) + ' npm-audit-main.json audit-docs: name: Audit Docs Site Package @@ -88,6 +93,11 @@ jobs: sarif_file: npm-audit-docs.sarif category: npm-audit-docs - - name: Run npm audit (fail on high/critical) - run: npm audit --audit-level=high + - name: Enforce npm audit (fail on high/critical) + run: | + jq -e ' + (.error | not) and + (.metadata.vulnerabilities.high == 0) and + (.metadata.vulnerabilities.critical == 0) + ' npm-audit-docs.json working-directory: docs-site From 21f50279c42312a0873113c05373f4f614245c80 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 18:33:20 -0700 Subject: [PATCH 06/11] ci: extend dependency audit timeout Allow time for npm mirror requests and SARIF processing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- .github/workflows/dependency-audit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index ac5fbac3c..35fb4a237 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -21,7 +21,7 @@ jobs: audit-main: name: Audit Main Package runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 steps: - name: Checkout repository @@ -61,7 +61,7 @@ jobs: audit-docs: name: Audit Docs Site Package runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 steps: - name: Checkout repository From f504244fb41538ae1710f2a5f1700f113cfdf64c Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 18:43:28 -0700 Subject: [PATCH 07/11] ci: retry failed npm audits upstream Fall back to registry.npmjs.org when the configured mirror returns an audit error. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- .github/workflows/dependency-audit.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 35fb4a237..4b02048d2 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -37,7 +37,12 @@ jobs: run: npm ci - name: Run npm audit (JSON output for SARIF) - run: npm audit --json > npm-audit-main.json || true + run: | + npm audit --json > npm-audit-main.json || true + if jq -e '.error' npm-audit-main.json >/dev/null; then + echo "::warning::Configured npm audit endpoint failed; retrying against registry.npmjs.org" + npm audit --registry=https://registry.npmjs.org --json > npm-audit-main.json || true + fi - name: Convert npm audit to SARIF if: always() @@ -79,7 +84,12 @@ jobs: working-directory: docs-site - name: Run npm audit (JSON output for SARIF) - run: npm audit --json > npm-audit-docs.json || true + run: | + npm audit --json > npm-audit-docs.json || true + if jq -e '.error' npm-audit-docs.json >/dev/null; then + echo "::warning::Configured npm audit endpoint failed; retrying against registry.npmjs.org" + npm audit --registry=https://registry.npmjs.org --json > npm-audit-docs.json || true + fi working-directory: docs-site - name: Convert npm audit to SARIF From 28897b877a14402c5e5d1ded324f4ecea082ac1d Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 18:59:03 -0700 Subject: [PATCH 08/11] ci: bound npm audit retries Retry mirror failures with short request timeouts before falling back upstream. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- .github/workflows/dependency-audit.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 4b02048d2..6f5ce67e8 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -38,10 +38,16 @@ jobs: - name: Run npm audit (JSON output for SARIF) run: | - npm audit --json > npm-audit-main.json || true + audit_args=(--json --fetch-retries=0 --fetch-timeout=30000) + npm audit "${audit_args[@]}" > npm-audit-main.json || true + if jq -e '.error' npm-audit-main.json >/dev/null; then + echo "::warning::Configured npm audit endpoint failed; retrying" + npm audit "${audit_args[@]}" > npm-audit-main.json || true + fi if jq -e '.error' npm-audit-main.json >/dev/null; then echo "::warning::Configured npm audit endpoint failed; retrying against registry.npmjs.org" - npm audit --registry=https://registry.npmjs.org --json > npm-audit-main.json || true + npm audit "${audit_args[@]}" --registry=https://registry.npmjs.org \ + > npm-audit-main.json || true fi - name: Convert npm audit to SARIF @@ -85,10 +91,16 @@ jobs: - name: Run npm audit (JSON output for SARIF) run: | - npm audit --json > npm-audit-docs.json || true + audit_args=(--json --fetch-retries=0 --fetch-timeout=30000) + npm audit "${audit_args[@]}" > npm-audit-docs.json || true + if jq -e '.error' npm-audit-docs.json >/dev/null; then + echo "::warning::Configured npm audit endpoint failed; retrying" + npm audit "${audit_args[@]}" > npm-audit-docs.json || true + fi if jq -e '.error' npm-audit-docs.json >/dev/null; then echo "::warning::Configured npm audit endpoint failed; retrying against registry.npmjs.org" - npm audit --registry=https://registry.npmjs.org --json > npm-audit-docs.json || true + npm audit "${audit_args[@]}" --registry=https://registry.npmjs.org \ + > npm-audit-docs.json || true fi working-directory: docs-site From d46a374028e8663978bc3b20fba99e6c41eaa39f Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 19:11:31 -0700 Subject: [PATCH 09/11] ci: audit npm lockfiles directly Avoid npm's retired quick-audit fallback for installed dependency trees. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- .github/workflows/dependency-audit.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 6f5ce67e8..34b01bfd2 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -38,12 +38,8 @@ jobs: - name: Run npm audit (JSON output for SARIF) run: | - audit_args=(--json --fetch-retries=0 --fetch-timeout=30000) + audit_args=(--package-lock-only --json --fetch-retries=0 --fetch-timeout=30000) npm audit "${audit_args[@]}" > npm-audit-main.json || true - if jq -e '.error' npm-audit-main.json >/dev/null; then - echo "::warning::Configured npm audit endpoint failed; retrying" - npm audit "${audit_args[@]}" > npm-audit-main.json || true - fi if jq -e '.error' npm-audit-main.json >/dev/null; then echo "::warning::Configured npm audit endpoint failed; retrying against registry.npmjs.org" npm audit "${audit_args[@]}" --registry=https://registry.npmjs.org \ @@ -91,12 +87,8 @@ jobs: - name: Run npm audit (JSON output for SARIF) run: | - audit_args=(--json --fetch-retries=0 --fetch-timeout=30000) + audit_args=(--package-lock-only --json --fetch-retries=0 --fetch-timeout=30000) npm audit "${audit_args[@]}" > npm-audit-docs.json || true - if jq -e '.error' npm-audit-docs.json >/dev/null; then - echo "::warning::Configured npm audit endpoint failed; retrying" - npm audit "${audit_args[@]}" > npm-audit-docs.json || true - fi if jq -e '.error' npm-audit-docs.json >/dev/null; then echo "::warning::Configured npm audit endpoint failed; retrying against registry.npmjs.org" npm audit "${audit_args[@]}" --registry=https://registry.npmjs.org \ From dbb44cffa85621bd10ca8e7d2b5bfc466ade068f Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 19:26:03 -0700 Subject: [PATCH 10/11] ci: avoid duplicate install-time audits Run one explicit lockfile audit after dependency installation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- .github/workflows/dependency-audit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 34b01bfd2..4e95202db 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -34,7 +34,7 @@ jobs: cache: 'npm' - name: Install dependencies - run: npm ci + run: npm ci --no-audit --no-fund - name: Run npm audit (JSON output for SARIF) run: | @@ -82,7 +82,7 @@ jobs: cache-dependency-path: docs-site/package-lock.json - name: Install dependencies - run: npm ci + run: npm ci --no-audit --no-fund working-directory: docs-site - name: Run npm audit (JSON output for SARIF) From 1b4643ccc414bbcbcdb4a34b755d869f59127ac5 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 3 Sep 2026 19:32:36 -0700 Subject: [PATCH 11/11] ci: tolerate audit service outages on PRs Warn when both advisory endpoints fail without masking vulnerability findings. Keep scheduled and main-branch audits fail-closed on service errors. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b4a13c54-417e-4b91-a9a4-08df7b66d644 --- .github/workflows/dependency-audit.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 4e95202db..621f9da80 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -58,9 +58,15 @@ jobs: category: npm-audit-main - name: Enforce npm audit (fail on high/critical) + env: + EVENT_NAME: ${{ github.event_name }} run: | + if jq -e '.error' npm-audit-main.json >/dev/null; then + echo "::warning::npm audit advisory service unavailable after retries" + test "$EVENT_NAME" = "pull_request" + exit + fi jq -e ' - (.error | not) and (.metadata.vulnerabilities.high == 0) and (.metadata.vulnerabilities.critical == 0) ' npm-audit-main.json @@ -108,9 +114,15 @@ jobs: category: npm-audit-docs - name: Enforce npm audit (fail on high/critical) + env: + EVENT_NAME: ${{ github.event_name }} run: | + if jq -e '.error' npm-audit-docs.json >/dev/null; then + echo "::warning::npm audit advisory service unavailable after retries" + test "$EVENT_NAME" = "pull_request" + exit + fi jq -e ' - (.error | not) and (.metadata.vulnerabilities.high == 0) and (.metadata.vulnerabilities.critical == 0) ' npm-audit-docs.json