From a385aaac1e7cbdaa53fcff3cfebfcd8501f405e2 Mon Sep 17 00:00:00 2001 From: Garrett Maring Date: Fri, 22 May 2026 12:58:59 -0300 Subject: [PATCH] V33 Gate 4: Harden ChatGPT App action contracts Add package-owned ChatGPT App Reading action contracts covering Read, Need, Finding Fits, preview, fee, settlement, and delivery actions with source-safe renderers, proof roots, denied-state repair posture, generated artifact checks, workflow wiring, and ChatGPT App registry integration. Validate with V33 Gate 1-4 checkers, V33 strict spec quality, BTD typecheck/full Jest, ChatGPT App typecheck/full Jest, protocol tests, canon posture/canonical-input checks, V32 promotion readiness, and diff/secret hygiene. --- .../v33-chatgpt-app-action-contracts.json | 540 ++++++++++++ .bitcode/v33-interface-contract-catalog.json | 4 +- .github/workflows/bitcode-gate-quality.yml | 4 +- BITCODE_SPEC_V33.md | 4 +- BITCODE_SPEC_V33_DELTA.md | 9 + BITCODE_SPEC_V33_NOTES.md | 9 + BITCODE_SPEC_V33_PARITY_MATRIX.md | 15 +- SPECIFICATIONS_ROADMAP.md | 2 +- package.json | 3 + .../chatgpt-app-action-contract.test.ts | 152 ++++ packages/btd/package.json | 1 + .../btd/src/chatgpt-app-action-contract.ts | 822 ++++++++++++++++++ packages/btd/src/index.ts | 1 + packages/chatgptapp/jest.config.cjs | 1 + .../__tests__/chatgpt-action-contract.test.ts | 112 +++ .../src/__tests__/yapperFlow.test.ts | 18 + packages/chatgptapp/src/tools.ts | 147 ++++ packages/chatgptapp/tsconfig.json | 1 + packages/chatgptapp/tsconfig.test.json | 1 + .../protocol/src/canonical/v21-specifying.js | 3 +- ...v33-gate4-chatgpt-app-action-contracts.mjs | 320 +++++++ ...erate-v33-chatgpt-app-action-contracts.mjs | 304 +++++++ 22 files changed, 2466 insertions(+), 7 deletions(-) create mode 100644 .bitcode/v33-chatgpt-app-action-contracts.json create mode 100644 packages/btd/__tests__/chatgpt-app-action-contract.test.ts create mode 100644 packages/btd/src/chatgpt-app-action-contract.ts create mode 100644 packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts create mode 100644 scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs create mode 100644 scripts/generate-v33-chatgpt-app-action-contracts.mjs diff --git a/.bitcode/v33-chatgpt-app-action-contracts.json b/.bitcode/v33-chatgpt-app-action-contracts.json new file mode 100644 index 000000000..76abeacce --- /dev/null +++ b/.bitcode/v33-chatgpt-app-action-contracts.json @@ -0,0 +1,540 @@ +{ + "artifactId": "v33-chatgpt-app-action-contracts", + "artifactRoot": "v33-chatgpt-app-action-contracts-artifact:8d008c4714bc4e9c4f883a35", + "contracts": [ + { + "actionId": "bitcode_request_read", + "authPolicyId": "interface.authorization.chatgpt-reading-action", + "contractRoot": "v33-chatgpt-app-action-contract:156de9b40f4d3da5a606a203", + "deniedStates": [ + "SCHEMA_VALIDATION_FAILED", + "MISSING_READER_SESSION" + ], + "examples": [ + "bitcode_request_read-success-source-safe", + "bitcode_request_read-denied-readable-repair" + ], + "flowObject": "read_request", + "inputSchemaId": "bitcode.chatgpt.bitcode_request_read.input.v1", + "outputSchemaId": "bitcode.chatgpt.bitcode_request_read.output.v1", + "proofRootFields": [ + "actionId", + "inputSchemaId", + "outputSchemaId", + "authPolicyId", + "requestRoot", + "responseRoot", + "sourceSafeRendererId", + "writeAdmission" + ], + "requestRootFields": [ + "readerId", + "repository", + "readRequest", + "requestId", + "interfaceSessionId", + "attachmentsRoot" + ], + "requiredPermissions": [ + "chatgpt.reading.invoke" + ], + "responseRootFields": [ + "readRequestRoot", + "readinessState", + "nextAction" + ], + "sourceSafeRendererId": "chatgpt.sourceSafeRenderer.bitcode_request_read", + "sourceSafetyClass": "source-safe-internal", + "validationCommands": [ + "pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/chatgpt-app-action-contract.test.ts", + "pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts src/__tests__/tools.test.ts --runInBand" + ] + }, + { + "actionId": "bitcode_review_read_need", + "authPolicyId": "interface.authorization.chatgpt-reading-action", + "contractRoot": "v33-chatgpt-app-action-contract:7f5d9f7d35f389520afa1d71", + "deniedStates": [ + "SCHEMA_VALIDATION_FAILED", + "MISSING_READER_SESSION", + "READ_NEED_REQUIRED" + ], + "examples": [ + "bitcode_review_read_need-success-source-safe", + "bitcode_review_read_need-denied-readable-repair" + ], + "flowObject": "read_need", + "inputSchemaId": "bitcode.chatgpt.bitcode_review_read_need.input.v1", + "outputSchemaId": "bitcode.chatgpt.bitcode_review_read_need.output.v1", + "proofRootFields": [ + "actionId", + "inputSchemaId", + "outputSchemaId", + "authPolicyId", + "requestRoot", + "responseRoot", + "sourceSafeRendererId", + "writeAdmission" + ], + "requestRootFields": [ + "readRequestRoot", + "readNeedRoot", + "decision", + "feedback" + ], + "requiredPermissions": [ + "chatgpt.reading.invoke" + ], + "responseRootFields": [ + "acceptedNeedRoot", + "reviewState", + "nextAction" + ], + "sourceSafeRendererId": "chatgpt.sourceSafeRenderer.bitcode_review_read_need", + "sourceSafetyClass": "source-safe-internal", + "validationCommands": [ + "pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/chatgpt-app-action-contract.test.ts", + "pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts src/__tests__/tools.test.ts --runInBand" + ] + }, + { + "actionId": "bitcode_request_finding_fits", + "authPolicyId": "interface.authorization.chatgpt-reading-action", + "contractRoot": "v33-chatgpt-app-action-contract:651d1c474b727bddc1cf86c9", + "deniedStates": [ + "SCHEMA_VALIDATION_FAILED", + "MISSING_READER_SESSION", + "READ_NEED_REQUIRED" + ], + "examples": [ + "bitcode_request_finding_fits-success-source-safe", + "bitcode_request_finding_fits-denied-readable-repair" + ], + "flowObject": "finding_fits_result", + "inputSchemaId": "bitcode.chatgpt.bitcode_request_finding_fits.input.v1", + "outputSchemaId": "bitcode.chatgpt.bitcode_request_finding_fits.output.v1", + "proofRootFields": [ + "actionId", + "inputSchemaId", + "outputSchemaId", + "authPolicyId", + "requestRoot", + "responseRoot", + "sourceSafeRendererId", + "writeAdmission" + ], + "requestRootFields": [ + "acceptedNeedRoot", + "depositoryScope", + "minimumFitConfidence", + "candidateLimit" + ], + "requiredPermissions": [ + "chatgpt.reading.invoke" + ], + "responseRootFields": [ + "findingFitsResultRoot", + "candidateCount", + "nextAction" + ], + "sourceSafeRendererId": "chatgpt.sourceSafeRenderer.bitcode_request_finding_fits", + "sourceSafetyClass": "source-safe-internal", + "validationCommands": [ + "pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/chatgpt-app-action-contract.test.ts", + "pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts src/__tests__/tools.test.ts --runInBand" + ] + }, + { + "actionId": "bitcode_review_asset_pack_preview", + "authPolicyId": "interface.authorization.chatgpt-reading-action", + "contractRoot": "v33-chatgpt-app-action-contract:f094aac1f01463e7b5a7d202", + "deniedStates": [ + "SCHEMA_VALIDATION_FAILED", + "MISSING_READER_SESSION", + "FINDING_FITS_REQUIRED", + "ASSET_PACK_PREVIEW_REQUIRED" + ], + "examples": [ + "bitcode_review_asset_pack_preview-success-source-safe", + "bitcode_review_asset_pack_preview-denied-readable-repair" + ], + "flowObject": "asset_pack_preview", + "inputSchemaId": "bitcode.chatgpt.bitcode_review_asset_pack_preview.input.v1", + "outputSchemaId": "bitcode.chatgpt.bitcode_review_asset_pack_preview.output.v1", + "proofRootFields": [ + "actionId", + "inputSchemaId", + "outputSchemaId", + "authPolicyId", + "requestRoot", + "responseRoot", + "sourceSafeRendererId", + "writeAdmission" + ], + "requestRootFields": [ + "findingFitsResultRoot", + "assetPackPreviewRoot", + "previewDecision" + ], + "requiredPermissions": [ + "chatgpt.reading.invoke" + ], + "responseRootFields": [ + "assetPackPreviewRoot", + "fitQuality", + "nextAction" + ], + "sourceSafeRendererId": "chatgpt.sourceSafeRenderer.bitcode_review_asset_pack_preview", + "sourceSafetyClass": "source-safe-internal", + "validationCommands": [ + "pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/chatgpt-app-action-contract.test.ts", + "pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts src/__tests__/tools.test.ts --runInBand" + ] + }, + { + "actionId": "bitcode_quote_asset_pack_fee", + "authPolicyId": "interface.authorization.chatgpt-reading-action", + "contractRoot": "v33-chatgpt-app-action-contract:2fa00a28da3e4f28c6d63968", + "deniedStates": [ + "SCHEMA_VALIDATION_FAILED", + "MISSING_READER_SESSION", + "ASSET_PACK_PREVIEW_REQUIRED" + ], + "examples": [ + "bitcode_quote_asset_pack_fee-success-source-safe", + "bitcode_quote_asset_pack_fee-denied-readable-repair" + ], + "flowObject": "btc_fee_quote", + "inputSchemaId": "bitcode.chatgpt.bitcode_quote_asset_pack_fee.input.v1", + "outputSchemaId": "bitcode.chatgpt.bitcode_quote_asset_pack_fee.output.v1", + "proofRootFields": [ + "actionId", + "inputSchemaId", + "outputSchemaId", + "authPolicyId", + "requestRoot", + "responseRoot", + "sourceSafeRendererId", + "writeAdmission" + ], + "requestRootFields": [ + "assetPackPreviewRoot", + "measurementVectorRoot", + "feePolicyId" + ], + "requiredPermissions": [ + "chatgpt.reading.invoke" + ], + "responseRootFields": [ + "feeQuoteRoot", + "btcAmountSats", + "nextAction" + ], + "sourceSafeRendererId": "chatgpt.sourceSafeRenderer.bitcode_quote_asset_pack_fee", + "sourceSafetyClass": "source-safe-internal", + "validationCommands": [ + "pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/chatgpt-app-action-contract.test.ts", + "pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts src/__tests__/tools.test.ts --runInBand" + ] + }, + { + "actionId": "bitcode_settle_asset_pack", + "authPolicyId": "interface.authorization.chatgpt-reading-action", + "contractRoot": "v33-chatgpt-app-action-contract:5aab0f91e5c29816518bd3fe", + "deniedStates": [ + "SCHEMA_VALIDATION_FAILED", + "MISSING_READER_SESSION", + "FEE_QUOTE_REQUIRED", + "CONFIRMATION_REQUIRED" + ], + "examples": [ + "bitcode_settle_asset_pack-success-source-safe", + "bitcode_settle_asset_pack-denied-readable-repair" + ], + "flowObject": "settlement_unlock", + "inputSchemaId": "bitcode.chatgpt.bitcode_settle_asset_pack.input.v1", + "outputSchemaId": "bitcode.chatgpt.bitcode_settle_asset_pack.output.v1", + "proofRootFields": [ + "actionId", + "inputSchemaId", + "outputSchemaId", + "authPolicyId", + "requestRoot", + "responseRoot", + "sourceSafeRendererId", + "writeAdmission" + ], + "requestRootFields": [ + "feeQuoteRoot", + "walletId", + "confirmationState", + "confirmed", + "btcPaymentIntentId" + ], + "requiredPermissions": [ + "chatgpt.reading.invoke" + ], + "responseRootFields": [ + "settlementRoot", + "finalityState", + "nextAction" + ], + "sourceSafeRendererId": "chatgpt.sourceSafeRenderer.bitcode_settle_asset_pack", + "sourceSafetyClass": "source-safe-internal", + "validationCommands": [ + "pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/chatgpt-app-action-contract.test.ts", + "pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts src/__tests__/tools.test.ts --runInBand" + ] + }, + { + "actionId": "bitcode_deliver_asset_pack", + "authPolicyId": "interface.authorization.chatgpt-reading-action", + "contractRoot": "v33-chatgpt-app-action-contract:c88122da0e7f4de4adbae5a6", + "deniedStates": [ + "SCHEMA_VALIDATION_FAILED", + "MISSING_READER_SESSION", + "SETTLEMENT_REQUIRED", + "READ_LICENSE_REQUIRED", + "ORGANIZATION_AUTHORITY_REQUIRED", + "CONFIRMATION_REQUIRED" + ], + "examples": [ + "bitcode_deliver_asset_pack-success-source-safe", + "bitcode_deliver_asset_pack-denied-readable-repair" + ], + "flowObject": "asset_pack_delivery", + "inputSchemaId": "bitcode.chatgpt.bitcode_deliver_asset_pack.input.v1", + "outputSchemaId": "bitcode.chatgpt.bitcode_deliver_asset_pack.output.v1", + "proofRootFields": [ + "actionId", + "inputSchemaId", + "outputSchemaId", + "authPolicyId", + "requestRoot", + "responseRoot", + "sourceSafeRendererId", + "writeAdmission" + ], + "requestRootFields": [ + "settlementRoot", + "readLicenseId", + "deliveryTarget", + "confirmed", + "organizationAuthorityRoot" + ], + "requiredPermissions": [ + "chatgpt.reading.invoke" + ], + "responseRootFields": [ + "deliveryRoot", + "deliveryState", + "nextAction" + ], + "sourceSafeRendererId": "chatgpt.sourceSafeRenderer.bitcode_deliver_asset_pack", + "sourceSafetyClass": "locked-assetpack-delivery", + "validationCommands": [ + "pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/chatgpt-app-action-contract.test.ts", + "pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts src/__tests__/tools.test.ts --runInBand" + ] + } + ], + "coverage": { + "credentialsSerialized": false, + "deniedStateCoverage": true, + "missingActionIds": [], + "missingFlowObjects": [], + "observedActionIds": [ + "bitcode_request_read", + "bitcode_review_read_need", + "bitcode_request_finding_fits", + "bitcode_review_asset_pack_preview", + "bitcode_quote_asset_pack_fee", + "bitcode_settle_asset_pack", + "bitcode_deliver_asset_pack" + ], + "observedFlowObjects": [ + "read_request", + "read_need", + "finding_fits_result", + "asset_pack_preview", + "btc_fee_quote", + "settlement_unlock", + "asset_pack_delivery" + ], + "proofRootCoverage": true, + "protectedSourceVisible": false, + "sourceEvidenceComplete": true, + "sourceSafeRendererCoverage": true, + "testEvidenceComplete": true + }, + "currentTarget": "V32", + "generatedAt": "2026-05-22T00:00:00.000Z", + "passed": true, + "requiredActionIds": [ + "bitcode_request_read", + "bitcode_review_read_need", + "bitcode_request_finding_fits", + "bitcode_review_asset_pack_preview", + "bitcode_quote_asset_pack_fee", + "bitcode_settle_asset_pack", + "bitcode_deliver_asset_pack" + ], + "requiredDeniedStates": [ + "SCHEMA_VALIDATION_FAILED", + "MISSING_READER_SESSION", + "READ_NEED_REQUIRED", + "FINDING_FITS_REQUIRED", + "ASSET_PACK_PREVIEW_REQUIRED", + "FEE_QUOTE_REQUIRED", + "SETTLEMENT_REQUIRED", + "READ_LICENSE_REQUIRED", + "ORGANIZATION_AUTHORITY_REQUIRED", + "CONFIRMATION_REQUIRED" + ], + "requiredFlowObjects": [ + "read_request", + "read_need", + "finding_fits_result", + "asset_pack_preview", + "btc_fee_quote", + "settlement_unlock", + "asset_pack_delivery" + ], + "requiredProofRootFields": [ + "actionId", + "inputSchemaId", + "outputSchemaId", + "authPolicyId", + "requestRoot", + "responseRoot", + "sourceSafeRendererId", + "writeAdmission" + ], + "schemaId": "bitcode.v33.chatGptAppActionContracts.v1", + "sourceEvidence": [ + { + "digest": "sha256:a24a464bd0910f8b8284ddfe56bfd846c36679b063b22546c409c136ff21254e", + "relativePath": "packages/btd/src/chatgpt-app-action-contract.ts", + "requiredTokens": [ + { + "present": true, + "token": "buildBtdChatGptAppActionContractRegistry" + }, + { + "present": true, + "token": "renderBtdChatGptAppSourceSafeResponse" + }, + { + "present": true, + "token": "bitcode_request_finding_fits" + }, + { + "present": true, + "token": "bitcode_deliver_asset_pack" + }, + { + "present": true, + "token": "SCHEMA_VALIDATION_FAILED" + }, + { + "present": true, + "token": "READ_LICENSE_REQUIRED" + } + ] + }, + { + "digest": "sha256:088af1a5895656f9d3e5f7bac544cf6549b87c6575b0388914f3695090e4599b", + "relativePath": "packages/chatgptapp/src/tools.ts", + "requiredTokens": [ + { + "present": true, + "token": "buildBtdChatGptAppActionContractRegistry" + }, + { + "present": true, + "token": "renderBtdChatGptAppSourceSafeResponse" + }, + { + "present": true, + "token": "getChatGptReadingActionTools" + }, + { + "present": true, + "token": "source-safe-action" + } + ] + } + ], + "sourceFiles": [ + { + "digest": "sha256:a24a464bd0910f8b8284ddfe56bfd846c36679b063b22546c409c136ff21254e", + "relativePath": "packages/btd/src/chatgpt-app-action-contract.ts" + }, + { + "digest": "sha256:813ac51681b167fda157744124a260e5ec7436b8864c6a742f953102b7a00a08", + "relativePath": "packages/btd/src/index.ts" + }, + { + "digest": "sha256:088af1a5895656f9d3e5f7bac544cf6549b87c6575b0388914f3695090e4599b", + "relativePath": "packages/chatgptapp/src/tools.ts" + } + ], + "sourceSafetyVerdict": "source-safe-chatgpt-app-action-contract-metadata", + "testEvidence": [ + { + "digest": "sha256:0106718de811ad1250860b075dd1c3203901f77a16071450969115539ff942cd", + "relativePath": "packages/btd/__tests__/chatgpt-app-action-contract.test.ts", + "requiredTokens": [ + { + "present": true, + "token": "publishes package-owned contracts for the full Reading action sequence" + }, + { + "present": true, + "token": "renders source-safe accepted responses with proof-root projection" + }, + { + "present": true, + "token": "renders readable denied responses with repair actions" + } + ] + }, + { + "digest": "sha256:e4cd9b57e9934c6c2c98f0c0e192142cac7d832e53c7893db8baaeb2c1dd18d6", + "relativePath": "packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts", + "requiredTokens": [ + { + "present": true, + "token": "registers every package-owned Reading action contract as a ChatGPT App tool" + }, + { + "present": true, + "token": "requires schema-valid arguments before executing package-owned actions" + }, + { + "present": true, + "token": "renders denied states with readable repair actions for ChatGPT App responses" + } + ] + } + ], + "testFiles": [ + { + "digest": "sha256:0106718de811ad1250860b075dd1c3203901f77a16071450969115539ff942cd", + "relativePath": "packages/btd/__tests__/chatgpt-app-action-contract.test.ts" + }, + { + "digest": "sha256:e4cd9b57e9934c6c2c98f0c0e192142cac7d832e53c7893db8baaeb2c1dd18d6", + "relativePath": "packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts" + }, + { + "digest": "sha256:1196162b7949e519c7277b0387c6506abc0813dcc6dad0aea73df7cfbab9e834", + "relativePath": "packages/chatgptapp/src/__tests__/tools.test.ts" + }, + { + "digest": "sha256:2463fbe32e67968ca76cb9d475fb71c0b02aa6f145bdd2d66f587c0f06efb931", + "relativePath": "scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs" + } + ], + "version": "V33" +} diff --git a/.bitcode/v33-interface-contract-catalog.json b/.bitcode/v33-interface-contract-catalog.json index 91c438292..ed4f2a4fe 100644 --- a/.bitcode/v33-interface-contract-catalog.json +++ b/.bitcode/v33-interface-contract-catalog.json @@ -222,7 +222,7 @@ ] }, { - "digest": "sha256:43b65036a542f5a329aec530008d3faabf404f7e9cd4082b69e59aa49bd6495c", + "digest": "sha256:813ac51681b167fda157744124a260e5ec7436b8864c6a742f953102b7a00a08", "relativePath": "packages/btd/src/index.ts", "requiredTokens": [ { @@ -232,7 +232,7 @@ ] }, { - "digest": "sha256:7b1a6d74f640a1a5d263f6863b7ed12c5bb1a12c45dd1ade2f5b9a878d3ae2d2", + "digest": "sha256:2b08cbe4ee9fade97e19398dbec9e84323df3248a220e7f6060c134bf0409c23", "relativePath": "BITCODE_SPEC_V33.md", "requiredTokens": [ { diff --git a/.github/workflows/bitcode-gate-quality.yml b/.github/workflows/bitcode-gate-quality.yml index 1ed8fb1ab..0648415fa 100644 --- a/.github/workflows/bitcode-gate-quality.yml +++ b/.github/workflows/bitcode-gate-quality.yml @@ -132,6 +132,7 @@ jobs: node scripts/check-v33-gate1-interface-roadmap-opening.mjs --skip-branch-check node scripts/check-v33-gate2-interface-contract-catalog.mjs --skip-branch-check node scripts/check-v33-gate3-mcp-api-tool-contracts.mjs --skip-branch-check + node scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs --skip-branch-check else echo "Unexpected BITCODE_SPEC.txt pointer: $POINTER" >&2 exit 1 @@ -161,13 +162,14 @@ jobs: pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/v32-interface-contract-regression.test.ts pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/interface-contract-catalog.test.ts pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/mcp-tool-contract.test.ts + pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/chatgpt-app-action-contract.test.ts pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/v32-testnet-mainnet-readiness-rehearsal.test.ts pnpm --dir packages/protocol exec node --test --test-force-exit test/v32-promotion-proof-generation.test.js pnpm --filter @bitcode/api exec jest --config jest.config.cjs --runTestsByPath src/routes/__tests__/btd-crypto.test.ts --runInBand pnpm --filter @bitcode/api exec jest --config jest.config.cjs --runTestsByPath src/routes/__tests__/auxillaries-contract.test.ts --runInBand pnpm --dir packages/executions-mcp/src/mcp-server run test:mcp -- --runTestsByPath src/__tests__/unit/auth.test.ts --runInBand pnpm --dir packages/executions-mcp/src/mcp-server run test:mcp -- --runTestsByPath src/__tests__/unit/mcp-tool-contract.test.ts src/__tests__/unit/pipeline-ingress-contract.test.ts --runInBand - pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/tools.test.ts --runInBand + pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts src/__tests__/tools.test.ts --runInBand pnpm --filter @bitcode/pipeline-hosts exec jest --config jest.config.cjs --runTestsByPath src/__tests__/asset-pack-harness.test.ts --runInBand pnpm --filter @bitcode/pipeline-asset-pack exec jest --config jest.config.cjs --passWithNoTests --forceExit pnpm --filter @bitcode/pipeline-asset-pack exec jest --config jest.config.cjs --runTestsByPath src/__tests__/reading-pipeline-observability.test.ts src/__tests__/reading-pipeline-contract.test.ts src/__tests__/v32-reading-pipeline-proof-coverage.test.ts --runInBand diff --git a/BITCODE_SPEC_V33.md b/BITCODE_SPEC_V33.md index f81823115..2337a3818 100644 --- a/BITCODE_SPEC_V33.md +++ b/BITCODE_SPEC_V33.md @@ -534,12 +534,14 @@ V33 inherits the V20 operator-quality expectation that interface-facing proof is | `.bitcode/v33-canonical-input-report.json` | canonical-input report builder | `check-bitcode-canonical-inputs` | source-safe-generated-proof | draft-required | | `.bitcode/v33-interface-contract-catalog.json` | `scripts/generate-v33-interface-contract-catalog.mjs` | `check:v33-interface-contract-catalog` and `check:v33-gate2` | source-safe-interface-contract-catalog-metadata | Gate 2 required | | `.bitcode/v33-mcp-api-tool-contracts.json` | `scripts/generate-v33-mcp-api-tool-contracts.mjs` | `check:v33-mcp-api-tool-contracts` and `check:v33-gate3` | source-safe-mcp-api-tool-contract-metadata | Gate 3 required | +| `.bitcode/v33-chatgpt-app-action-contracts.json` | `scripts/generate-v33-chatgpt-app-action-contracts.mjs` | `check:v33-chatgpt-app-action-contracts` and `check:v33-gate4` | source-safe-chatgpt-app-action-contract-metadata | Gate 4 required | ### V33 specifying generated artifacts V33 starts with `.bitcode/v33-spec-family-report.json` and `.bitcode/v33-canonical-input-report.json`. Gate 2 adds `.bitcode/v33-interface-contract-catalog.json`, which serializes source-safe `InterfaceContractCatalog` metadata for `terminal_handoff`, `public_api`, `mcp_api`, `chatgpt_app`, `package_consumer`, `exchange_hook`, and `conversations_hook` with deferred hooks marked `deferred_not_admitted`. Gate 3 adds `.bitcode/v33-mcp-api-tool-contracts.json`, which serializes source-safe `McpToolContract` metadata for `bitcode://pipelines/asset-pack/create`, including schema ids, denied states, proof-root fields, examples, package-derived discovery posture, and protected-source invisibility. +Gate 4 adds `.bitcode/v33-chatgpt-app-action-contracts.json`, which serializes source-safe `ChatGptAppActionContract` metadata for `bitcode_request_read`, `bitcode_review_read_need`, `bitcode_request_finding_fits`, `bitcode_review_asset_pack_preview`, `bitcode_quote_asset_pack_fee`, `bitcode_settle_asset_pack`, and `bitcode_deliver_asset_pack`, including package-owned schemas, source-safe response renderers, proof-root projection, readable denial states such as `READ_LICENSE_REQUIRED`, and repair actions. Later gates may add authorization policy, schema compatibility, telemetry replay, and promotion readiness artifacts. ### Shared generated-artifact fields @@ -564,7 +566,7 @@ V33 promotion must fail closed when generated artifacts are missing, stale, sour ## Appendix D. Validation and checking gate catalog -V33 validation includes `check-bitcode-spec-family`, `check-bitcode-canonical-inputs`, `check-bitcode-canon-posture-drift`, `check:v33-gate1`, later gate-specific checkers, package tests, interface contract tests, and promotion workflow checks. +V33 validation includes `check-bitcode-spec-family`, `check-bitcode-canonical-inputs`, `check-bitcode-canon-posture-drift`, `check:v33-gate1`, `check:v33-gate2`, `check:v33-gate3`, `check:v33-gate4`, later gate-specific checkers, package tests, interface contract tests, and promotion workflow checks. ## Appendix E. Current canonical source map diff --git a/BITCODE_SPEC_V33_DELTA.md b/BITCODE_SPEC_V33_DELTA.md index 9c15b9cac..3fdd23b00 100644 --- a/BITCODE_SPEC_V33_DELTA.md +++ b/BITCODE_SPEC_V33_DELTA.md @@ -111,6 +111,15 @@ Implementation centers: Gate 4 hardens ChatGPT App contracts. +Implementation centers: + +- `packages/btd/src/chatgpt-app-action-contract.ts` owns `ChatGptAppActionContract`, `BTD_CHATGPT_APP_ACTION_CONTRACT_IDS`, and source-safe response rendering through `renderBtdChatGptAppSourceSafeResponse`; +- `packages/chatgptapp/src/tools.ts` derives Reading action tool names, descriptions, schemas, source-safe metadata, and denial repair posture from `buildBtdChatGptAppActionContractRegistry`; +- the package-owned action ids are `bitcode_request_read`, `bitcode_review_read_need`, `bitcode_request_finding_fits`, `bitcode_review_asset_pack_preview`, `bitcode_quote_asset_pack_fee`, `bitcode_settle_asset_pack`, and `bitcode_deliver_asset_pack`; +- denied states include `SCHEMA_VALIDATION_FAILED`, `MISSING_READER_SESSION`, `READ_NEED_REQUIRED`, `FINDING_FITS_REQUIRED`, `ASSET_PACK_PREVIEW_REQUIRED`, `FEE_QUOTE_REQUIRED`, `SETTLEMENT_REQUIRED`, `READ_LICENSE_REQUIRED`, `ORGANIZATION_AUTHORITY_REQUIRED`, and `CONFIRMATION_REQUIRED`; +- `.bitcode/v33-chatgpt-app-action-contracts.json` records source-safe Gate 4 action metadata, proof-root coverage, source-safe renderer coverage, denial repair coverage, and credential non-serialization; +- `check:v33-gate4` validates source, tests, docs, workflow, generated artifact freshness, and ChatGPT App source-safe action posture. + Closure acceptance: - ChatGPT App actions use package-owned schemas and source-safe response renderers; diff --git a/BITCODE_SPEC_V33_NOTES.md b/BITCODE_SPEC_V33_NOTES.md index 10dfe8d56..39c160b4b 100644 --- a/BITCODE_SPEC_V33_NOTES.md +++ b/BITCODE_SPEC_V33_NOTES.md @@ -78,6 +78,15 @@ Denied states are explicit and include `SCHEMA_VALIDATION_FAILED`, `PROVIDER_BIN The MCP server consumes the package-owned contract through `getBtdMcpToolContract` for tool discovery, including tool id and description, while pre-settlement protected source remains invisible. The generated source-safe artifact is `.bitcode/v33-mcp-api-tool-contracts.json`, checked by `check:v33-mcp-api-tool-contracts` and `check:v33-gate3`. +## Gate 4 closure note + +Gate 4 introduces `ChatGptAppActionContract` in `packages/btd/src/chatgpt-app-action-contract.ts`. +The required ChatGPT App Reading action ids are `bitcode_request_read`, `bitcode_review_read_need`, `bitcode_request_finding_fits`, `bitcode_review_asset_pack_preview`, `bitcode_quote_asset_pack_fee`, `bitcode_settle_asset_pack`, and `bitcode_deliver_asset_pack`. +Each action binds package-owned input/output schemas, `interface.authorization.chatgpt-reading-action`, `chatgpt.reading.invoke`, a source-safe response renderer, proof-root projection, and readable repair posture. +Denied states include `SCHEMA_VALIDATION_FAILED`, `READ_NEED_REQUIRED`, `FINDING_FITS_REQUIRED`, `ASSET_PACK_PREVIEW_REQUIRED`, `FEE_QUOTE_REQUIRED`, `SETTLEMENT_REQUIRED`, `READ_LICENSE_REQUIRED`, `ORGANIZATION_AUTHORITY_REQUIRED`, and `CONFIRMATION_REQUIRED`. +The ChatGPT App tool registry consumes the package-owned contracts through `buildBtdChatGptAppActionContractRegistry`, while `renderBtdChatGptAppSourceSafeResponse` keeps locked AssetPack contents invisible. +The generated source-safe artifact is `.bitcode/v33-chatgpt-app-action-contracts.json`, checked by `check:v33-chatgpt-app-action-contracts` and `check:v33-gate4`. + ## Carryforward from V32 V33 inherits these V32 truths: diff --git a/BITCODE_SPEC_V33_PARITY_MATRIX.md b/BITCODE_SPEC_V33_PARITY_MATRIX.md index 0dcf984ba..749f9df76 100644 --- a/BITCODE_SPEC_V33_PARITY_MATRIX.md +++ b/BITCODE_SPEC_V33_PARITY_MATRIX.md @@ -53,7 +53,7 @@ No `_legacy/` source is active source truth. | Roadmap truth | Gate 1 | `SPECIFICATIONS_ROADMAP.md`, README, PR template, workflow posture | drafted | Roadmap states V32 active, V33 draft, and coherent V34-V37 responsibilities. | | Interface contract catalog | Gate 2 | `packages/btd/src/interface-contract-catalog.ts`, `packages/btd/__tests__/interface-contract-catalog.test.ts`, `.bitcode/v33-interface-contract-catalog.json`, `check:v33-gate2` | drafted | Active and deferred interface surfaces have package-owned rows. | | MCP API contracts | Gate 3 | `packages/btd/src/mcp-tool-contract.ts`, `packages/btd/__tests__/mcp-tool-contract.test.ts`, `packages/executions-mcp/src/mcp-server/src/tools/pipeline-tools.ts`, `packages/executions-mcp/src/mcp-server/src/__tests__/unit/mcp-tool-contract.test.ts`, `.bitcode/v33-mcp-api-tool-contracts.json`, `check:v33-gate3` | drafted | MCP tool discovery, schemas, auth, denied states, and proof roots are package-derived. | -| ChatGPT App contracts | Gate 4 | planned ChatGPT App action contract tests and examples | pending | ChatGPT App actions match package-owned Read, Need, Finding Fits, preview, fee, settlement, and delivery contracts. | +| ChatGPT App contracts | Gate 4 | `packages/btd/src/chatgpt-app-action-contract.ts`, `packages/btd/__tests__/chatgpt-app-action-contract.test.ts`, `packages/chatgptapp/src/tools.ts`, `packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts`, `.bitcode/v33-chatgpt-app-action-contracts.json`, `check:v33-gate4` | drafted | ChatGPT App actions match package-owned Read, Need, Finding Fits, preview, fee, settlement, and delivery contracts. | | Interface authorization policy | Gate 5 | planned `InterfaceAuthorizationPolicy` fixtures and tests | pending | Interface auth and license denials fail closed with repair posture. | | Read license and AssetPack rights contracts | Gate 6 | planned `ReadLicenseInterfaceContract` and `AssetPackRightsInterfaceContract` tests | pending | Source-safe preview, paid settlement, BTD rights, and delivery contracts are consistent across interfaces. | | API schema compatibility | Gate 7 | planned `APISchemaCompatibilityMatrix` artifact and tests | pending | Schemas, examples, compatibility status, and validation commands are source-safe and versionless. | @@ -100,6 +100,19 @@ No `_legacy/` source is active source truth. | Tests fail closed | BTD and MCP tests cover contract discovery, proof roots, denied states, invalid schema arguments, and protected-source invisibility | drafted | | Workflow and spec-family wiring exists | `check:v33-gate3`, `.github/workflows/bitcode-gate-quality.yml`, and `packages/protocol/src/canonical/v21-specifying.js` include the Gate 3 contract | drafted | +## Gate 4 Parity + +| Requirement | Source evidence | Current V33 judgment | +| --- | --- | --- | +| Package-owned ChatGPT action contract builder exists | `packages/btd/src/chatgpt-app-action-contract.ts` exports `buildBtdChatGptAppActionContractRegistry`, `BTD_CHATGPT_APP_ACTION_CONTRACT_IDS`, and `renderBtdChatGptAppSourceSafeResponse` | drafted | +| Reading action sequence is canonical | `bitcode_request_read`, `bitcode_review_read_need`, `bitcode_request_finding_fits`, `bitcode_review_asset_pack_preview`, `bitcode_quote_asset_pack_fee`, `bitcode_settle_asset_pack`, and `bitcode_deliver_asset_pack` are required action ids | drafted | +| Schemas and auth are bound | every `ChatGptAppActionContract` carries input/output schema ids, `interface.authorization.chatgpt-reading-action`, `chatgpt.reading.invoke`, and source-safe renderer id | drafted | +| Denied states are explicit and repairable | `SCHEMA_VALIDATION_FAILED`, `READ_NEED_REQUIRED`, `FINDING_FITS_REQUIRED`, `ASSET_PACK_PREVIEW_REQUIRED`, `FEE_QUOTE_REQUIRED`, `SETTLEMENT_REQUIRED`, `READ_LICENSE_REQUIRED`, `ORGANIZATION_AUTHORITY_REQUIRED`, and `CONFIRMATION_REQUIRED` are readable repair states | drafted | +| ChatGPT App consumes package contract | `packages/chatgptapp/src/tools.ts` uses `buildBtdChatGptAppActionContractRegistry`, derives tool schemas, and returns `renderBtdChatGptAppSourceSafeResponse` metadata | drafted | +| Generated artifact is source-safe and deterministic | `.bitcode/v33-chatgpt-app-action-contracts.json` is produced by `generate:v33-chatgpt-app-action-contracts` and checked by `check:v33-chatgpt-app-action-contracts` | drafted | +| Tests fail closed | BTD and ChatGPT tests cover action registration, schemas, proof roots, accepted source-safe render output, denied repair actions, and secret-shaped source text rejection | drafted | +| Workflow and spec-family wiring exists | `check:v33-gate4`, `.github/workflows/bitcode-gate-quality.yml`, and `packages/protocol/src/canonical/v21-specifying.js` include the Gate 4 contract | drafted | + ## Gate 1 Parity | Requirement | Source evidence | Current V33 judgment | diff --git a/SPECIFICATIONS_ROADMAP.md b/SPECIFICATIONS_ROADMAP.md index 7d0e31af9..29969fc97 100644 --- a/SPECIFICATIONS_ROADMAP.md +++ b/SPECIFICATIONS_ROADMAP.md @@ -5,7 +5,7 @@ - Current active canonical pointer: `BITCODE_SPEC.txt` -> `V32` - Current active canon: `BITCODE_SPEC_V32.md` - Current draft target: `BITCODE_SPEC_V33.md` -- Current working gate: V33 Gate 3 MCP API Tool And Registry Contracts, which makes MCP tool discovery package-derived and source-safe, binds `bitcode://pipelines/asset-pack/create` to `McpToolContract`, proves schema/auth denial states such as `SCHEMA_VALIDATION_FAILED` and `PROVIDER_BINDING_REQUIRED`, and generates `.bitcode/v33-mcp-api-tool-contracts.json`. +- Current working gate: V33 Gate 4 ChatGPT App Action And Tool Contracts, which makes ChatGPT App Reading actions package-derived and source-safe, binds Read, Need, Finding Fits, preview, fee, settlement, and delivery action ids to `ChatGptAppActionContract`, proves denial repair states such as `READ_LICENSE_REQUIRED` and `SETTLEMENT_REQUIRED`, and generates `.bitcode/v33-chatgpt-app-action-contracts.json`. - Purpose: concise running index of Bitcode/ENGI specification history, current work, and planned work. This roadmap is not an active system specification. diff --git a/package.json b/package.json index a5ee53a27..3b0f4097b 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,9 @@ "generate:v33-mcp-api-tool-contracts": "node scripts/generate-v33-mcp-api-tool-contracts.mjs", "check:v33-mcp-api-tool-contracts": "node scripts/generate-v33-mcp-api-tool-contracts.mjs --check", "check:v33-gate3": "node scripts/check-v33-gate3-mcp-api-tool-contracts.mjs", + "generate:v33-chatgpt-app-action-contracts": "node scripts/generate-v33-chatgpt-app-action-contracts.mjs", + "check:v33-chatgpt-app-action-contracts": "node scripts/generate-v33-chatgpt-app-action-contracts.mjs --check", + "check:v33-gate4": "node scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs", "check:spec-quality": "node scripts/run-bitcode-spec-quality.mjs --mode basic", "check:spec-quality:title": "node scripts/run-bitcode-spec-quality.mjs --mode strict-from-title", "check:spec-quality:v24": "node scripts/run-bitcode-spec-quality.mjs --mode strict-version --version V24", diff --git a/packages/btd/__tests__/chatgpt-app-action-contract.test.ts b/packages/btd/__tests__/chatgpt-app-action-contract.test.ts new file mode 100644 index 000000000..0ea37dada --- /dev/null +++ b/packages/btd/__tests__/chatgpt-app-action-contract.test.ts @@ -0,0 +1,152 @@ +import { + BTD_CHATGPT_APP_ACTION_CONTRACT_IDS, + BTD_CHATGPT_APP_ACTION_REQUIRED_PROOF_ROOT_FIELDS, + BTD_CHATGPT_APP_REQUIRED_FLOW_OBJECTS, + buildBtdChatGptAppActionContract, + buildBtdChatGptAppActionContractInputs, + buildBtdChatGptAppActionContractRegistry, + getBtdChatGptAppActionContract, + renderBtdChatGptAppSourceSafeResponse, +} from '../src/chatgpt-app-action-contract'; + +describe('ChatGPT App action contract registry', () => { + it('publishes package-owned contracts for the full Reading action sequence', () => { + const registry = buildBtdChatGptAppActionContractRegistry(); + + expect(registry.kind).toBe('btd.chatgpt_app_action_contract_registry'); + expect(registry.schemaId).toBe('bitcode.chatGptAppActionContractRegistry.v1'); + expect(registry.actionCount).toBe(7); + expect(registry.missingActionIds).toEqual([]); + expect(registry.observedActionIds).toEqual([...BTD_CHATGPT_APP_ACTION_CONTRACT_IDS].sort()); + expect(registry.sourceSafety).toMatchObject({ + sourceSafe: true, + protectedSourceVisible: false, + containsSecret: false, + containsProtectedSource: false, + }); + + expect(registry.actions.map((action) => action.flowObject)).toEqual( + expect.arrayContaining([...BTD_CHATGPT_APP_REQUIRED_FLOW_OBJECTS]), + ); + }); + + it('carries schemas, proof roots, source-safe renderers, denied states, and examples', () => { + const contract = getBtdChatGptAppActionContract('bitcode_deliver_asset_pack'); + + expect(contract.contractRoot).toMatch(/^btd-chatgpt-app-action-contract:[a-f0-9]{24}$/); + expect(contract.inputSchemaId).toBe('bitcode.chatgpt.bitcode_deliver_asset_pack.input.v1'); + expect(contract.outputSchemaId).toBe('bitcode.chatgpt.bitcode_deliver_asset_pack.output.v1'); + expect(contract.sourceSafetyClass).toBe('locked-assetpack-delivery'); + expect(contract.proofRootFields).toEqual( + expect.arrayContaining([...BTD_CHATGPT_APP_ACTION_REQUIRED_PROOF_ROOT_FIELDS]), + ); + expect(contract.requestRootFields).toEqual( + expect.arrayContaining(['confirmed', 'deliveryTarget', 'readLicenseId', 'settlementRoot']), + ); + expect(contract.responseRootFields).toEqual( + expect.arrayContaining(['deliveryRoot', 'deliveryState', 'nextAction']), + ); + expect(contract.deniedStates.map((state) => state.code)).toEqual( + expect.arrayContaining([ + 'SCHEMA_VALIDATION_FAILED', + 'SETTLEMENT_REQUIRED', + 'READ_LICENSE_REQUIRED', + 'ORGANIZATION_AUTHORITY_REQUIRED', + 'CONFIRMATION_REQUIRED', + ]), + ); + expect(contract.examples.map((example) => example.posture)).toEqual( + expect.arrayContaining(['success_source_safe', 'denied_delivery']), + ); + }); + + it('renders source-safe accepted responses with proof-root projection', () => { + const response = renderBtdChatGptAppSourceSafeResponse({ + actionId: 'bitcode_review_asset_pack_preview', + status: 'accepted', + requestRoot: 'request-root-preview', + summary: 'AssetPack preview accepted for fee quotation.', + visibleFields: { + assetPackPreviewRoot: 'asset-pack-preview-root', + fitQuality: 'excellent', + nextAction: 'quote-fee', + }, + proofRootProjection: { + writeAdmission: 'not-required-for-preview-review', + }, + }); + + expect(response).toMatchObject({ + kind: 'btd.chatgpt_app_source_safe_response', + actionId: 'bitcode_review_asset_pack_preview', + flowObject: 'asset_pack_preview', + status: 'accepted', + sourceSafety: { + sourceSafe: true, + protectedSourceVisible: false, + containsSecret: false, + containsProtectedSource: false, + }, + proofRootProjection: expect.objectContaining({ + actionId: 'bitcode_review_asset_pack_preview', + requestRoot: 'request-root-preview', + sourceSafeRendererId: 'chatgpt.sourceSafeRenderer.bitcode_review_asset_pack_preview', + }), + }); + const forbiddenSourcePattern = new RegExp( + `raw source|private source|${['sk', 'proj'].join('-')}|${['sb', 'secret'].join('_')}__`, + 'i', + ); + expect(JSON.stringify(response.visibleFields)).not.toMatch(forbiddenSourcePattern); + }); + + it('renders readable denied responses with repair actions', () => { + const response = renderBtdChatGptAppSourceSafeResponse({ + actionId: 'bitcode_deliver_asset_pack', + status: 'denied', + deniedStateCode: 'SETTLEMENT_REQUIRED', + requestRoot: 'request-root-delivery', + visibleFields: { + deliveryState: 'blocked', + }, + }); + + expect(response.status).toBe('denied'); + expect(response.summary).toContain('Settlement finality is required'); + expect(response.deniedState).toMatchObject({ + code: 'SETTLEMENT_REQUIRED', + repairAction: 'wait-for-settlement-finality-and-readback-synchronization', + }); + expect(response.repairActions).toEqual([ + 'wait-for-settlement-finality-and-readback-synchronization', + ]); + }); + + it('fails closed when a required ChatGPT action id is missing', () => { + expect(() => buildBtdChatGptAppActionContractRegistry({ actions: [] })).toThrow( + /missing action ids: bitcode_request_read/, + ); + }); + + it('fails closed when proof roots omit a required field', () => { + const [input] = buildBtdChatGptAppActionContractInputs(); + + expect(() => + buildBtdChatGptAppActionContract({ + ...input, + proofRootFields: ['actionId'], + }), + ).toThrow(/missing proof-root fields/); + }); + + it('fails closed on secret-shaped or locked-content contract text', () => { + const [input] = buildBtdChatGptAppActionContractInputs(); + + expect(() => + buildBtdChatGptAppActionContract({ + ...input, + description: 'raw source can be shown in ChatGPT', + }), + ).toThrow(/must not contain secrets or locked source contents/); + }); +}); diff --git a/packages/btd/package.json b/packages/btd/package.json index 36d2c729e..b94ed86a1 100644 --- a/packages/btd/package.json +++ b/packages/btd/package.json @@ -6,6 +6,7 @@ "types": "src/index.ts", "exports": { ".": "./src/index.ts", + "./chatgpt-app-action-contract": "./src/chatgpt-app-action-contract.ts", "./interface-integration-contract": "./src/interface-integration-contract.ts", "./terminal-operational-health": "./src/terminal-operational-health.ts" }, diff --git a/packages/btd/src/chatgpt-app-action-contract.ts b/packages/btd/src/chatgpt-app-action-contract.ts new file mode 100644 index 000000000..9cea36faf --- /dev/null +++ b/packages/btd/src/chatgpt-app-action-contract.ts @@ -0,0 +1,822 @@ +import { createHash } from 'crypto'; +import { assertNonEmptyString } from './constants'; +import type { BtdProtocolTelemetrySourceSafety } from './telemetry'; + +export const BTD_CHATGPT_APP_ACTION_CONTRACT_IDS = [ + 'bitcode_request_read', + 'bitcode_review_read_need', + 'bitcode_request_finding_fits', + 'bitcode_review_asset_pack_preview', + 'bitcode_quote_asset_pack_fee', + 'bitcode_settle_asset_pack', + 'bitcode_deliver_asset_pack', +] as const; + +export type BtdChatGptAppActionId = (typeof BTD_CHATGPT_APP_ACTION_CONTRACT_IDS)[number]; + +export type BtdChatGptAppActionFlowObject = + | 'read_request' + | 'read_need' + | 'finding_fits_result' + | 'asset_pack_preview' + | 'btc_fee_quote' + | 'settlement_unlock' + | 'asset_pack_delivery'; + +export type BtdChatGptAppActionSourceSafetyClass = + | 'source-safe-public' + | 'source-safe-internal' + | 'locked-assetpack-delivery'; + +export type BtdChatGptAppActionDeniedStateCode = + | 'SCHEMA_VALIDATION_FAILED' + | 'MISSING_READER_SESSION' + | 'READ_NEED_REQUIRED' + | 'FINDING_FITS_REQUIRED' + | 'ASSET_PACK_PREVIEW_REQUIRED' + | 'FEE_QUOTE_REQUIRED' + | 'SETTLEMENT_REQUIRED' + | 'READ_LICENSE_REQUIRED' + | 'ORGANIZATION_AUTHORITY_REQUIRED' + | 'CONFIRMATION_REQUIRED'; + +export type BtdChatGptAppActionExamplePosture = + | 'success_source_safe' + | 'denied_schema' + | 'denied_repair' + | 'denied_settlement' + | 'denied_delivery'; + +export interface BtdChatGptAppActionDeniedStateInput { + code: BtdChatGptAppActionDeniedStateCode; + readableMessage: string; + repairAction: string; +} + +export interface BtdChatGptAppActionDeniedState extends BtdChatGptAppActionDeniedStateInput { + deniedStateRoot: string; +} + +export interface BtdChatGptAppActionExampleInput { + exampleId: string; + posture: BtdChatGptAppActionExamplePosture; + fixturePath: string; + validationCommand: string; + sourceSafetyClass: BtdChatGptAppActionSourceSafetyClass; +} + +export interface BtdChatGptAppActionExample extends BtdChatGptAppActionExampleInput { + exampleRoot: string; +} + +export interface BtdChatGptAppActionContractInput { + actionId: BtdChatGptAppActionId; + flowObject: BtdChatGptAppActionFlowObject; + summary: string; + description: string; + uiResponseFamily: string; + inputSchemaId: string; + outputSchemaId: string; + authPolicyId: string; + requiredPermissions: readonly string[]; + sourceSafetyClass: BtdChatGptAppActionSourceSafetyClass; + sourceSafeRendererId: string; + inputSchema: Record; + outputSchema: Record; + proofRootFields: readonly string[]; + requestRootFields: readonly string[]; + responseRootFields: readonly string[]; + deniedStates: readonly BtdChatGptAppActionDeniedStateInput[]; + examples: readonly BtdChatGptAppActionExampleInput[]; +} + +export interface BtdChatGptAppActionContract { + kind: 'btd.chatgpt_app_action_contract'; + actionId: BtdChatGptAppActionId; + flowObject: BtdChatGptAppActionFlowObject; + summary: string; + description: string; + uiResponseFamily: string; + inputSchemaId: string; + outputSchemaId: string; + authPolicyId: string; + requiredPermissions: string[]; + sourceSafetyClass: BtdChatGptAppActionSourceSafetyClass; + sourceSafeRendererId: string; + inputSchema: Record; + outputSchema: Record; + proofRootFields: string[]; + requestRootFields: string[]; + responseRootFields: string[]; + deniedStates: BtdChatGptAppActionDeniedState[]; + examples: BtdChatGptAppActionExample[]; + contractRoot: string; + sourceSafety: BtdProtocolTelemetrySourceSafety; +} + +export interface BtdChatGptAppActionContractRegistryInput { + actions?: readonly BtdChatGptAppActionContractInput[]; + requiredActionIds?: readonly BtdChatGptAppActionId[]; +} + +export interface BtdChatGptAppActionContractRegistry { + kind: 'btd.chatgpt_app_action_contract_registry'; + schemaId: 'bitcode.chatGptAppActionContractRegistry.v1'; + registryRoot: string; + actionCount: number; + requiredActionIds: BtdChatGptAppActionId[]; + observedActionIds: BtdChatGptAppActionId[]; + missingActionIds: BtdChatGptAppActionId[]; + actions: BtdChatGptAppActionContract[]; + sourceSafety: BtdProtocolTelemetrySourceSafety; +} + +export interface BtdChatGptAppSourceSafeResponseInput { + actionId: BtdChatGptAppActionId; + status: 'accepted' | 'denied'; + requestRoot: string; + responseRoot?: string; + summary?: string; + visibleFields?: Record; + deniedStateCode?: BtdChatGptAppActionDeniedStateCode; + proofRootProjection?: Record; +} + +export interface BtdChatGptAppSourceSafeResponse { + kind: 'btd.chatgpt_app_source_safe_response'; + actionId: BtdChatGptAppActionId; + flowObject: BtdChatGptAppActionFlowObject; + uiResponseFamily: string; + status: 'accepted' | 'denied'; + summary: string; + visibleFields: Record; + repairActions: string[]; + deniedState?: BtdChatGptAppActionDeniedState; + proofRootProjection: Record; + responseRoot: string; + sourceSafety: BtdProtocolTelemetrySourceSafety; +} + +const SOURCE_SAFETY: BtdProtocolTelemetrySourceSafety = { + sourceSafe: true, + protectedSourceVisible: false, + containsProtectedSource: false, + containsSecret: false, +}; + +const REQUIRED_PROOF_ROOT_FIELDS = [ + 'actionId', + 'inputSchemaId', + 'outputSchemaId', + 'authPolicyId', + 'requestRoot', + 'responseRoot', + 'sourceSafeRendererId', + 'writeAdmission', +] as const; + +const REQUIRED_FLOW_OBJECTS: readonly BtdChatGptAppActionFlowObject[] = [ + 'read_request', + 'read_need', + 'finding_fits_result', + 'asset_pack_preview', + 'btc_fee_quote', + 'settlement_unlock', + 'asset_pack_delivery', +]; + +const SECRET_OR_SOURCE_PATTERNS = [ + new RegExp(`${['sb', 'secret'].join('_')}__`, 'iu'), + /\bsk-(?:proj|live|test)?[-_A-Za-z0-9]{16,}\b/u, + /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/u, + /-----BEGIN [A-Z ]*PRIVATE KEY-----/u, + /\bprivate\s+source\b/iu, + /\braw\s+source\b/iu, +]; + +export const BTD_CHATGPT_APP_ACTION_REQUIRED_PROOF_ROOT_FIELDS = [ + ...REQUIRED_PROOF_ROOT_FIELDS, +] as const; + +export const BTD_CHATGPT_APP_REQUIRED_FLOW_OBJECTS = [...REQUIRED_FLOW_OBJECTS] as const; + +export function buildBtdChatGptAppActionContractInputs(): BtdChatGptAppActionContractInput[] { + return [ + buildActionInput({ + actionId: 'bitcode_request_read', + flowObject: 'read_request', + summary: 'Request a Bitcode Read from ChatGPT with source-safe repository intent.', + description: + 'Captures the user Read request, repository anchor, and requested work shape for ReadNeedComprehensionSynthesis without exposing AssetPack contents.', + uiResponseFamily: 'read-request-admission', + requiredFields: ['readerId', 'repository', 'readRequest'], + optionalFields: ['requestId', 'interfaceSessionId', 'attachmentsRoot'], + responseFields: ['readRequestRoot', 'readinessState', 'nextAction'], + deniedStates: baseDeniedStates(), + }), + buildActionInput({ + actionId: 'bitcode_review_read_need', + flowObject: 'read_need', + summary: 'Review, accept, or request regeneration of a synthesized ReadNeed.', + description: + 'Binds a reviewed ReadNeed root to the ChatGPT App conversation so Finding Fits can only run from user-approved need comprehension.', + uiResponseFamily: 'read-need-review', + requiredFields: ['readRequestRoot', 'readNeedRoot', 'decision'], + optionalFields: ['feedback'], + responseFields: ['acceptedNeedRoot', 'reviewState', 'nextAction'], + deniedStates: [ + ...baseDeniedStates(), + { + code: 'READ_NEED_REQUIRED', + readableMessage: 'A synthesized ReadNeed must be present before review can continue.', + repairAction: 'run-read-need-comprehension-or-provide-read-need-root', + }, + ], + }), + buildActionInput({ + actionId: 'bitcode_request_finding_fits', + flowObject: 'finding_fits_result', + summary: 'Request plural Finding Fits over the Depository from an accepted ReadNeed.', + description: + 'Admits ReadFitsFindingSynthesis from an accepted ReadNeed root and returns source-safe candidate and fit-quality metadata only.', + uiResponseFamily: 'finding-fits-admission', + requiredFields: ['acceptedNeedRoot', 'depositoryScope'], + optionalFields: ['minimumFitConfidence', 'candidateLimit'], + responseFields: ['findingFitsResultRoot', 'candidateCount', 'nextAction'], + deniedStates: [ + ...baseDeniedStates(), + { + code: 'READ_NEED_REQUIRED', + readableMessage: 'Finding Fits requires an accepted ReadNeed root.', + repairAction: 'accept-read-need-before-requesting-finding-fits', + }, + ], + }), + buildActionInput({ + actionId: 'bitcode_review_asset_pack_preview', + flowObject: 'asset_pack_preview', + summary: 'Review a source-safe AssetPack preview produced from Finding Fits.', + description: + 'Surfaces AssetPack measurements, fit score, and preview metadata while locked AssetPack contents remain unavailable before settlement.', + uiResponseFamily: 'assetpack-preview-review', + requiredFields: ['findingFitsResultRoot', 'assetPackPreviewRoot'], + optionalFields: ['previewDecision'], + responseFields: ['assetPackPreviewRoot', 'fitQuality', 'nextAction'], + deniedStates: [ + ...baseDeniedStates(), + { + code: 'FINDING_FITS_REQUIRED', + readableMessage: 'An accepted Finding Fits result is required before preview review.', + repairAction: 'request-finding-fits-before-preview-review', + }, + { + code: 'ASSET_PACK_PREVIEW_REQUIRED', + readableMessage: 'A source-safe AssetPack preview root is required for preview review.', + repairAction: 'synthesize-source-safe-assetpack-preview', + }, + ], + }), + buildActionInput({ + actionId: 'bitcode_quote_asset_pack_fee', + flowObject: 'btc_fee_quote', + summary: 'Quote the BTC fee for a source-safe AssetPack preview.', + description: + 'Projects deterministic measurement-vector fee inputs and BTC payment posture without disclosing locked AssetPack contents.', + uiResponseFamily: 'assetpack-fee-quote', + requiredFields: ['assetPackPreviewRoot', 'measurementVectorRoot'], + optionalFields: ['feePolicyId'], + responseFields: ['feeQuoteRoot', 'btcAmountSats', 'nextAction'], + deniedStates: [ + ...baseDeniedStates(), + { + code: 'ASSET_PACK_PREVIEW_REQUIRED', + readableMessage: 'A source-safe AssetPack preview is required before fee quoting.', + repairAction: 'review-source-safe-assetpack-preview-before-fee-quote', + }, + ], + }), + buildActionInput({ + actionId: 'bitcode_settle_asset_pack', + flowObject: 'settlement_unlock', + summary: 'Admit settlement for a quoted AssetPack fee.', + description: + 'Records settlement intent and BTC finality posture for the quoted AssetPack fee while keeping delivery locked until settlement and read-license evidence agree.', + uiResponseFamily: 'assetpack-settlement', + requiredFields: ['feeQuoteRoot', 'walletId', 'confirmationState', 'confirmed'], + optionalFields: ['btcPaymentIntentId'], + responseFields: ['settlementRoot', 'finalityState', 'nextAction'], + deniedStates: [ + ...baseDeniedStates(), + { + code: 'FEE_QUOTE_REQUIRED', + readableMessage: 'A fee quote root is required before settlement.', + repairAction: 'quote-assetpack-fee-before-settlement', + }, + { + code: 'CONFIRMATION_REQUIRED', + readableMessage: 'Settlement requires explicit user confirmation in ChatGPT.', + repairAction: 'confirm-settlement-intent-before-continuing', + }, + ], + }), + buildActionInput({ + actionId: 'bitcode_deliver_asset_pack', + flowObject: 'asset_pack_delivery', + summary: 'Admit full AssetPack delivery only after settlement and read-license evidence.', + description: + 'Projects source-safe delivery admission for the paid AssetPack; ChatGPT receives delivery status, proof roots, and repair posture, not locked contents.', + uiResponseFamily: 'assetpack-delivery', + requiredFields: ['settlementRoot', 'readLicenseId', 'deliveryTarget', 'confirmed'], + optionalFields: ['organizationAuthorityRoot'], + responseFields: ['deliveryRoot', 'deliveryState', 'nextAction'], + sourceSafetyClass: 'locked-assetpack-delivery', + deniedStates: [ + ...baseDeniedStates(), + { + code: 'SETTLEMENT_REQUIRED', + readableMessage: 'Settlement finality is required before full AssetPack delivery.', + repairAction: 'wait-for-settlement-finality-and-readback-synchronization', + }, + { + code: 'READ_LICENSE_REQUIRED', + readableMessage: 'A paid read license is required before delivery admission.', + repairAction: 'refresh-read-license-evidence-after-settlement', + }, + { + code: 'ORGANIZATION_AUTHORITY_REQUIRED', + readableMessage: 'Organization authority evidence is required for connected-interface delivery.', + repairAction: 'refresh-organization-role-wallet-and-delivery-permission-evidence', + }, + { + code: 'CONFIRMATION_REQUIRED', + readableMessage: 'Delivery requires explicit user confirmation in ChatGPT.', + repairAction: 'confirm-delivery-target-before-continuing', + }, + ], + }), + ]; +} + +export function buildBtdChatGptAppActionContract( + input: BtdChatGptAppActionContractInput, +): BtdChatGptAppActionContract { + const actionId = assertActionId(input.actionId); + const deniedStates = input.deniedStates.map(buildDeniedState); + const examples = input.examples.map(buildExample); + const proofRootFields = assertProofRootFields(input.proofRootFields); + const requestRootFields = assertStringList(input.requestRootFields, 'requestRootField'); + const responseRootFields = assertStringList(input.responseRootFields, 'responseRootField'); + const inputSchema = assertSourceSafeRecord(input.inputSchema, 'inputSchema'); + const outputSchema = assertSourceSafeRecord(input.outputSchema, 'outputSchema'); + + if (!input.requiredPermissions.includes('chatgpt.reading.invoke')) { + throw new Error(`${actionId} must require chatgpt.reading.invoke permission.`); + } + if (!deniedStates.some((state) => state.code === 'SCHEMA_VALIDATION_FAILED')) { + throw new Error(`${actionId} must carry a schema-validation denied state.`); + } + if (!examples.some((example) => example.posture === 'success_source_safe')) { + throw new Error(`${actionId} must carry a source-safe success example.`); + } + if (!examples.some((example) => example.posture.startsWith('denied_'))) { + throw new Error(`${actionId} must carry at least one denied-state example.`); + } + + const contract = { + kind: 'btd.chatgpt_app_action_contract' as const, + actionId, + flowObject: assertFlowObject(input.flowObject), + summary: assertSourceSafeString(input.summary, 'summary'), + description: assertSourceSafeString(input.description, 'description'), + uiResponseFamily: assertSourceSafeString(input.uiResponseFamily, 'uiResponseFamily'), + inputSchemaId: assertSourceSafeString(input.inputSchemaId, 'inputSchemaId'), + outputSchemaId: assertSourceSafeString(input.outputSchemaId, 'outputSchemaId'), + authPolicyId: assertSourceSafeString(input.authPolicyId, 'authPolicyId'), + requiredPermissions: assertStringList(input.requiredPermissions, 'requiredPermission'), + sourceSafetyClass: assertSourceSafetyClass(input.sourceSafetyClass), + sourceSafeRendererId: assertSourceSafeString(input.sourceSafeRendererId, 'sourceSafeRendererId'), + inputSchema, + outputSchema, + proofRootFields, + requestRootFields, + responseRootFields, + deniedStates, + examples, + sourceSafety: { ...SOURCE_SAFETY }, + }; + + return { + ...contract, + contractRoot: stableRoot('btd-chatgpt-app-action-contract', [ + contract.actionId, + contract.flowObject, + contract.summary, + contract.description, + contract.uiResponseFamily, + contract.inputSchemaId, + contract.outputSchemaId, + contract.authPolicyId, + contract.requiredPermissions.join(','), + contract.sourceSafetyClass, + contract.sourceSafeRendererId, + stableJson(contract.inputSchema), + stableJson(contract.outputSchema), + contract.proofRootFields.join(','), + contract.requestRootFields.join(','), + contract.responseRootFields.join(','), + contract.deniedStates.map((state) => state.deniedStateRoot).join(','), + contract.examples.map((example) => example.exampleRoot).join(','), + ]), + }; +} + +export function buildBtdChatGptAppActionContractRegistry( + input: BtdChatGptAppActionContractRegistryInput = {}, +): BtdChatGptAppActionContractRegistry { + const actions = (input.actions ?? buildBtdChatGptAppActionContractInputs()).map( + buildBtdChatGptAppActionContract, + ); + const requiredActionIds = [...(input.requiredActionIds ?? BTD_CHATGPT_APP_ACTION_CONTRACT_IDS)]; + const observedActionIds = Array.from(new Set(actions.map((action) => action.actionId))).sort(); + const missingActionIds = requiredActionIds.filter((actionId) => !observedActionIds.includes(actionId)); + const duplicates = findDuplicates(actions.map((action) => action.actionId)); + const observedFlowObjects = Array.from(new Set(actions.map((action) => action.flowObject))); + const missingFlowObjects = REQUIRED_FLOW_OBJECTS.filter((flowObject) => !observedFlowObjects.includes(flowObject)); + + if (missingActionIds.length) { + throw new Error(`ChatGPT App action contract registry missing action ids: ${missingActionIds.join(', ')}.`); + } + if (duplicates.length) { + throw new Error(`ChatGPT App action contract registry contains duplicate action ids: ${duplicates.join(', ')}.`); + } + if (missingFlowObjects.length) { + throw new Error(`ChatGPT App action contract registry missing flow objects: ${missingFlowObjects.join(', ')}.`); + } + + return { + kind: 'btd.chatgpt_app_action_contract_registry', + schemaId: 'bitcode.chatGptAppActionContractRegistry.v1', + registryRoot: stableRoot('btd-chatgpt-app-action-contract-registry', [ + ...actions.map((action) => action.contractRoot), + requiredActionIds.join(','), + ]), + actionCount: actions.length, + requiredActionIds, + observedActionIds, + missingActionIds, + actions, + sourceSafety: { ...SOURCE_SAFETY }, + }; +} + +export function getBtdChatGptAppActionContract( + actionId: BtdChatGptAppActionId, +): BtdChatGptAppActionContract { + const contract = buildBtdChatGptAppActionContractRegistry().actions.find( + (action) => action.actionId === actionId, + ); + if (!contract) { + throw new Error(`ChatGPT App action contract not found: ${actionId}.`); + } + + return contract; +} + +export function renderBtdChatGptAppSourceSafeResponse( + input: BtdChatGptAppSourceSafeResponseInput, +): BtdChatGptAppSourceSafeResponse { + const contract = getBtdChatGptAppActionContract(input.actionId); + const requestRoot = assertSourceSafeString(input.requestRoot, 'requestRoot'); + const visibleFields = assertSourceSafeRecord(input.visibleFields ?? {}, 'visibleFields'); + const proofRootProjection = assertProofRootProjection(input.proofRootProjection ?? {}); + const deniedState = input.status === 'denied' + ? findDeniedState(contract, input.deniedStateCode) + : undefined; + const summary = assertSourceSafeString( + input.summary ?? + (input.status === 'denied' + ? deniedState?.readableMessage + : contract.summary), + 'summary', + ); + const responseRoot = input.responseRoot + ? assertSourceSafeString(input.responseRoot, 'responseRoot') + : stableRoot('btd-chatgpt-app-source-safe-response', [ + contract.actionId, + input.status, + requestRoot, + summary, + stableJson(visibleFields), + stableJson(proofRootProjection), + deniedState?.deniedStateRoot ?? 'accepted', + ]); + + return { + kind: 'btd.chatgpt_app_source_safe_response', + actionId: contract.actionId, + flowObject: contract.flowObject, + uiResponseFamily: contract.uiResponseFamily, + status: input.status, + summary, + visibleFields, + repairActions: deniedState ? [deniedState.repairAction] : [], + deniedState, + proofRootProjection: { + ...proofRootProjection, + actionId: contract.actionId, + inputSchemaId: contract.inputSchemaId, + outputSchemaId: contract.outputSchemaId, + authPolicyId: contract.authPolicyId, + requestRoot, + responseRoot, + sourceSafeRendererId: contract.sourceSafeRendererId, + writeAdmission: proofRootProjection.writeAdmission ?? 'not-required-for-source-safe-chatgpt-render', + }, + responseRoot, + sourceSafety: { ...SOURCE_SAFETY }, + }; +} + +function buildActionInput(input: { + actionId: BtdChatGptAppActionId; + flowObject: BtdChatGptAppActionFlowObject; + summary: string; + description: string; + uiResponseFamily: string; + requiredFields: string[]; + optionalFields: string[]; + responseFields: string[]; + deniedStates: BtdChatGptAppActionDeniedStateInput[]; + sourceSafetyClass?: BtdChatGptAppActionSourceSafetyClass; +}): BtdChatGptAppActionContractInput { + const allInputFields = [...input.requiredFields, ...input.optionalFields]; + + return { + actionId: input.actionId, + flowObject: input.flowObject, + summary: input.summary, + description: input.description, + uiResponseFamily: input.uiResponseFamily, + inputSchemaId: `bitcode.chatgpt.${input.actionId}.input.v1`, + outputSchemaId: `bitcode.chatgpt.${input.actionId}.output.v1`, + authPolicyId: 'interface.authorization.chatgpt-reading-action', + requiredPermissions: ['chatgpt.reading.invoke'], + sourceSafetyClass: input.sourceSafetyClass ?? 'source-safe-internal', + sourceSafeRendererId: `chatgpt.sourceSafeRenderer.${input.actionId}`, + inputSchema: objectSchemaFromFields(allInputFields, input.requiredFields), + outputSchema: objectSchemaFromFields(input.responseFields, input.responseFields), + proofRootFields: REQUIRED_PROOF_ROOT_FIELDS, + requestRootFields: allInputFields, + responseRootFields: input.responseFields, + deniedStates: input.deniedStates, + examples: [ + { + exampleId: `${input.actionId}-success-source-safe`, + posture: 'success_source_safe', + fixturePath: 'packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts', + validationCommand: + 'pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts --runInBand', + sourceSafetyClass: input.sourceSafetyClass ?? 'source-safe-internal', + }, + { + exampleId: `${input.actionId}-denied-readable-repair`, + posture: input.actionId === 'bitcode_deliver_asset_pack' ? 'denied_delivery' : 'denied_repair', + fixturePath: 'packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts', + validationCommand: + 'pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts --runInBand', + sourceSafetyClass: 'source-safe-internal', + }, + ], + }; +} + +function baseDeniedStates(): BtdChatGptAppActionDeniedStateInput[] { + return [ + { + code: 'SCHEMA_VALIDATION_FAILED', + readableMessage: 'The ChatGPT App action input did not match the package-owned schema.', + repairAction: 'repair-action-arguments-to-package-schema', + }, + { + code: 'MISSING_READER_SESSION', + readableMessage: 'The ChatGPT App action requires a reader session root.', + repairAction: 'refresh-reader-session-before-continuing', + }, + ]; +} + +function objectSchemaFromFields(fields: readonly string[], required: readonly string[]): Record { + const properties = Object.fromEntries( + fields.map((field) => [ + field, + { + type: field === 'candidateLimit' ? 'integer' : field === 'minimumFitConfidence' ? 'number' : field === 'confirmed' ? 'boolean' : 'string', + description: `${field} bound to the package-owned ChatGPT App action contract.`, + }, + ]), + ); + + return { + type: 'object', + additionalProperties: false, + properties, + required: [...required], + }; +} + +function buildDeniedState( + input: BtdChatGptAppActionDeniedStateInput, +): BtdChatGptAppActionDeniedState { + const state = { + code: assertDeniedStateCode(input.code), + readableMessage: assertSourceSafeString(input.readableMessage, 'readableMessage'), + repairAction: assertSourceSafeString(input.repairAction, 'repairAction'), + }; + + return { + ...state, + deniedStateRoot: stableRoot('btd-chatgpt-app-denied-state', [ + state.code, + state.readableMessage, + state.repairAction, + ]), + }; +} + +function buildExample(input: BtdChatGptAppActionExampleInput): BtdChatGptAppActionExample { + const example = { + exampleId: assertSourceSafeString(input.exampleId, 'exampleId'), + posture: assertExamplePosture(input.posture), + fixturePath: assertSourceSafeString(input.fixturePath, 'fixturePath'), + validationCommand: assertSourceSafeString(input.validationCommand, 'validationCommand'), + sourceSafetyClass: assertSourceSafetyClass(input.sourceSafetyClass), + }; + + return { + ...example, + exampleRoot: stableRoot('btd-chatgpt-app-action-example', [ + example.exampleId, + example.posture, + example.fixturePath, + example.validationCommand, + example.sourceSafetyClass, + ]), + }; +} + +function findDeniedState( + contract: BtdChatGptAppActionContract, + code: BtdChatGptAppActionDeniedStateCode | undefined, +): BtdChatGptAppActionDeniedState { + const resolvedCode = code ?? 'SCHEMA_VALIDATION_FAILED'; + const deniedState = contract.deniedStates.find((state) => state.code === resolvedCode); + if (!deniedState) { + throw new Error(`${contract.actionId} does not define ChatGPT denied state ${resolvedCode}.`); + } + + return deniedState; +} + +function assertActionId(actionId: string): BtdChatGptAppActionId { + if (!BTD_CHATGPT_APP_ACTION_CONTRACT_IDS.includes(actionId as BtdChatGptAppActionId)) { + throw new Error(`Unsupported ChatGPT App action contract id: ${actionId}.`); + } + + return actionId as BtdChatGptAppActionId; +} + +function assertFlowObject(flowObject: string): BtdChatGptAppActionFlowObject { + if (!REQUIRED_FLOW_OBJECTS.includes(flowObject as BtdChatGptAppActionFlowObject)) { + throw new Error(`Unsupported ChatGPT App flow object: ${flowObject}.`); + } + + return flowObject as BtdChatGptAppActionFlowObject; +} + +function assertDeniedStateCode(code: string): BtdChatGptAppActionDeniedStateCode { + const allowed: readonly BtdChatGptAppActionDeniedStateCode[] = [ + 'SCHEMA_VALIDATION_FAILED', + 'MISSING_READER_SESSION', + 'READ_NEED_REQUIRED', + 'FINDING_FITS_REQUIRED', + 'ASSET_PACK_PREVIEW_REQUIRED', + 'FEE_QUOTE_REQUIRED', + 'SETTLEMENT_REQUIRED', + 'READ_LICENSE_REQUIRED', + 'ORGANIZATION_AUTHORITY_REQUIRED', + 'CONFIRMATION_REQUIRED', + ]; + if (!allowed.includes(code as BtdChatGptAppActionDeniedStateCode)) { + throw new Error(`Unsupported ChatGPT App denied state code: ${code}.`); + } + + return code as BtdChatGptAppActionDeniedStateCode; +} + +function assertExamplePosture(posture: string): BtdChatGptAppActionExamplePosture { + const allowed: readonly BtdChatGptAppActionExamplePosture[] = [ + 'success_source_safe', + 'denied_schema', + 'denied_repair', + 'denied_settlement', + 'denied_delivery', + ]; + if (!allowed.includes(posture as BtdChatGptAppActionExamplePosture)) { + throw new Error(`Unsupported ChatGPT App example posture: ${posture}.`); + } + + return posture as BtdChatGptAppActionExamplePosture; +} + +function assertSourceSafetyClass( + sourceSafetyClass: string, +): BtdChatGptAppActionSourceSafetyClass { + const allowed: readonly BtdChatGptAppActionSourceSafetyClass[] = [ + 'source-safe-public', + 'source-safe-internal', + 'locked-assetpack-delivery', + ]; + if (!allowed.includes(sourceSafetyClass as BtdChatGptAppActionSourceSafetyClass)) { + throw new Error(`Unsupported ChatGPT App source-safety class: ${sourceSafetyClass}.`); + } + + return sourceSafetyClass as BtdChatGptAppActionSourceSafetyClass; +} + +function assertProofRootFields(fields: readonly string[]): string[] { + const deduped = assertStringList(fields, 'proofRootField'); + const missing = REQUIRED_PROOF_ROOT_FIELDS.filter((field) => !deduped.includes(field)); + if (missing.length) { + throw new Error(`ChatGPT App action contract missing proof-root fields: ${missing.join(', ')}.`); + } + + return deduped; +} + +function assertStringList(values: readonly string[], label: string): string[] { + if (!Array.isArray(values) || values.length === 0) { + throw new Error(`${label} requires at least one value.`); + } + + return Array.from(new Set(values.map((value) => assertSourceSafeString(value, label)))).sort(); +} + +function assertProofRootProjection(values: Record): Record { + return Object.fromEntries( + Object.entries(values).map(([key, value]) => [ + assertSourceSafeString(key, 'proofRootProjectionKey'), + assertSourceSafeString(value, 'proofRootProjectionValue'), + ]), + ); +} + +function assertSourceSafeRecord(value: Record, label: string): Record { + const text = stableJson(value); + if (SECRET_OR_SOURCE_PATTERNS.some((pattern) => pattern.test(text))) { + throw new Error(`${label} must not contain secrets or locked source contents.`); + } + + return value; +} + +function assertSourceSafeString(value: unknown, label: string): string { + const text = assertNonEmptyString(value, label); + if (SECRET_OR_SOURCE_PATTERNS.some((pattern) => pattern.test(text))) { + throw new Error(`${label} must not contain secrets or locked source contents.`); + } + + return text; +} + +function findDuplicates(values: readonly string[]): string[] { + const seen = new Set(); + const duplicate = new Set(); + for (const value of values) { + if (seen.has(value)) duplicate.add(value); + seen.add(value); + } + + return [...duplicate].sort(); +} + +function stableJson(value: unknown): string { + return JSON.stringify(sortJson(value)); +} + +function sortJson(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sortJson); + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value as Record) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, entry]) => [key, sortJson(entry)]), + ); + } + + return value; +} + +function stableRoot(prefix: string, parts: string[]): string { + const hash = createHash('sha256').update(parts.join('\u001f')).digest('hex').slice(0, 24); + return `${prefix}:${hash}`; +} diff --git a/packages/btd/src/index.ts b/packages/btd/src/index.ts index ce86fa573..19edc8276 100644 --- a/packages/btd/src/index.ts +++ b/packages/btd/src/index.ts @@ -158,6 +158,7 @@ export * from './bitcoin-fees'; export * from './btc-fee-operation'; export * from './bitcoin-provider'; export * from './bridge-readiness'; +export * from './chatgpt-app-action-contract'; export * from './access'; export * from './allocation'; export * from './ancestry'; diff --git a/packages/chatgptapp/jest.config.cjs b/packages/chatgptapp/jest.config.cjs index b895a14d5..0123f1ecb 100644 --- a/packages/chatgptapp/jest.config.cjs +++ b/packages/chatgptapp/jest.config.cjs @@ -1,6 +1,7 @@ const tsJestPreset = require('ts-jest/presets/js-with-ts/jest-preset'); const customModuleNameMapper = { '^@bitcode/btd$': '/../btd/src/authority.ts', + '^@bitcode/btd/chatgpt-app-action-contract$': '/../btd/src/chatgpt-app-action-contract.ts', '^@bitcode/tools-generics$': '/src/__stubs__/tools-generics.ts', '^@bitcode/prompts/prompt$': '/../prompts/src/prompt.ts', '^@bitcode/prompts/parts/PromptPart$': '/../prompts/src/parts/PromptPart.ts', diff --git a/packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts b/packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts new file mode 100644 index 000000000..4423f35a3 --- /dev/null +++ b/packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts @@ -0,0 +1,112 @@ +import { + BTD_CHATGPT_APP_ACTION_CONTRACT_IDS, + getBtdChatGptAppActionContract, + renderBtdChatGptAppSourceSafeResponse, +} from '@bitcode/btd/chatgpt-app-action-contract'; +import { getBitcodeTools } from '../tools'; + +describe('ChatGPT App Reading action contracts', () => { + const tools = getBitcodeTools(); + + const findTool = (name: string) => { + const tool = tools.find((candidate) => candidate.name === name); + if (!tool) throw new Error(`Missing ChatGPT App tool ${name}`); + return tool; + }; + + it('registers every package-owned Reading action contract as a ChatGPT App tool', () => { + for (const actionId of BTD_CHATGPT_APP_ACTION_CONTRACT_IDS) { + const contract = getBtdChatGptAppActionContract(actionId); + const tool = findTool(actionId); + + expect(tool.name).toBe(contract.actionId); + expect(tool.description).toBe(contract.description); + expect(tool.inputSchema).toEqual(contract.inputSchema); + expect(tool.meta).toMatchObject({ + categories: ['reading', 'source-safe-action'], + contractRoot: contract.contractRoot, + flowObject: contract.flowObject, + sourceSafeRendererId: contract.sourceSafeRendererId, + }); + } + }); + + it('executes Read request action with source-safe response rendering and proof roots', async () => { + const tool = findTool('bitcode_request_read'); + const result = await tool.execute({ + readerId: 'reader-1', + repository: 'engineeredsoftware/ENGI', + readRequest: 'Repair the staged Reading flow.', + requestId: 'read-request-1', + }); + + expect(result).toMatchObject({ + result: { + readRequestRoot: expect.stringMatching(/^chatgpt-readRequestRoot:[a-f0-9]{24}$/), + readinessState: 'admitted-for-read-need-comprehension', + nextAction: 'synthesize-read-need', + }, + metadata: { + actionContract: { + actionId: 'bitcode_request_read', + flowObject: 'read_request', + inputSchemaId: 'bitcode.chatgpt.bitcode_request_read.input.v1', + }, + sourceSafeResponse: { + kind: 'btd.chatgpt_app_source_safe_response', + status: 'accepted', + sourceSafety: { + sourceSafe: true, + protectedSourceVisible: false, + containsSecret: false, + containsProtectedSource: false, + }, + proofRootProjection: expect.objectContaining({ + actionId: 'bitcode_request_read', + requestRoot: expect.stringMatching(/^chatgpt-action-request:[a-f0-9]{24}$/), + sourceSafeRendererId: 'chatgpt.sourceSafeRenderer.bitcode_request_read', + }), + }, + }, + }); + const forbiddenSourcePattern = new RegExp( + `raw source|private source|${['sk', 'proj'].join('-')}|${['sb', 'secret'].join('_')}__`, + 'i', + ); + expect(JSON.stringify(result)).not.toMatch(forbiddenSourcePattern); + }); + + it('requires schema-valid arguments before executing package-owned actions', () => { + const tool = findTool('bitcode_request_finding_fits'); + + expect(tool.validator.safeParse({ depositoryScope: 'all-deposits' }).success).toBe(false); + expect( + tool.validator.safeParse({ + acceptedNeedRoot: 'accepted-need-root', + depositoryScope: 'all-deposits', + minimumFitConfidence: 0.76, + candidateLimit: 8, + }).success, + ).toBe(true); + }); + + it('renders denied states with readable repair actions for ChatGPT App responses', () => { + const response = renderBtdChatGptAppSourceSafeResponse({ + actionId: 'bitcode_deliver_asset_pack', + status: 'denied', + deniedStateCode: 'READ_LICENSE_REQUIRED', + requestRoot: 'delivery-request-root', + visibleFields: { + deliveryState: 'blocked', + }, + }); + + expect(response.status).toBe('denied'); + expect(response.summary).toContain('paid read license'); + expect(response.repairActions).toEqual(['refresh-read-license-evidence-after-settlement']); + expect(response.proofRootProjection).toMatchObject({ + actionId: 'bitcode_deliver_asset_pack', + authPolicyId: 'interface.authorization.chatgpt-reading-action', + }); + }); +}); diff --git a/packages/chatgptapp/src/__tests__/yapperFlow.test.ts b/packages/chatgptapp/src/__tests__/yapperFlow.test.ts index 004e9ef92..cff4cfbf8 100644 --- a/packages/chatgptapp/src/__tests__/yapperFlow.test.ts +++ b/packages/chatgptapp/src/__tests__/yapperFlow.test.ts @@ -26,6 +26,20 @@ const simpleSystemTextSearchExecute = const webSearchExecute = (jest.requireMock('@bitcode/generic-tools-web-search').search.execute as jest.Mock); const generateDigestMock = (jest.requireMock('@bitcode/digest/run').generateDigest as jest.Mock); +const READ_ACCESS = { + assetPackId: 'asset-pack-yapper', + walletId: 'wallet-yapper-reader', + decision: 'licensed_read', + accessPolicyHash: 'policy-hash-yapper', + reason: 'wallet_has_valid_policy_matching_license', +} as const; +const ORGANIZATION_AUTHORITY = { + organizationId: 'org-yapper', + organizationRole: 'member', + organizationPermissionGrants: ['asset_pack:deliver'], + walletId: 'wallet-yapper-reader', + settlementState: 'settled', +} as const; describe('Yapper demo flow', () => { beforeEach(() => { @@ -193,6 +207,8 @@ This product delivers voice-first social conversations for builders. const awsWrite = await runTool<{ metadata: { guidance: string; writeAdmission: Record } }>('use_aws_write_external_mcp', { request: 's3.putObject', confirmed: true, + readAccess: READ_ACCESS, + organizationAuthority: ORGANIZATION_AUTHORITY, payload: { bucket: 'bitcode-yapper', key: 'config/demo.json', body: '{}' }, }); expect(awsWrite.metadata.guidance).toContain('Uploaded to S3'); @@ -206,6 +222,8 @@ This product delivers voice-first social conversations for builders. const vercelDeploy = await runTool<{ result: { readyState: string }; metadata: { writeAdmission: Record } }>('use_vercel_write_external_mcp', { request: 'deploy_to_vercel', confirmed: true, + readAccess: READ_ACCESS, + organizationAuthority: ORGANIZATION_AUTHORITY, payload: { projectId: 'prj_Yapper', teamId: 'team_bitcode', message: 'Preview deploy after optimistic UI work.' }, }); expect((vercelDeploy.result as any).readyState).toBe('BUILDING'); diff --git a/packages/chatgptapp/src/tools.ts b/packages/chatgptapp/src/tools.ts index ed39ae621..ca4a969b0 100644 --- a/packages/chatgptapp/src/tools.ts +++ b/packages/chatgptapp/src/tools.ts @@ -1,6 +1,7 @@ import './env'; import { Buffer } from 'node:buffer'; +import { createHash } from 'node:crypto'; import path from 'node:path'; import { prepareConciseContext } from '@bitcode/context'; @@ -14,6 +15,12 @@ import { type BtdOrganizationRole, type BtdSettlementAuthorityState, } from '@bitcode/btd'; +import { + buildBtdChatGptAppActionContractRegistry, + renderBtdChatGptAppSourceSafeResponse, + type BtdChatGptAppActionContract, + type BtdChatGptAppActionId, +} from '@bitcode/btd/chatgpt-app-action-contract'; import { vercelGetDeploymentEventsTool, vercelGetDeploymentTool, @@ -1459,12 +1466,152 @@ const AWS_WRITE_SCHEMA = { required: ['request', 'confirmed', 'readAccess', 'organizationAuthority'] }; +const CHATGPT_READING_ACTION_NEXT_ACTION: Record = { + bitcode_request_read: 'synthesize-read-need', + bitcode_review_read_need: 'request-finding-fits', + bitcode_request_finding_fits: 'review-assetpack-preview', + bitcode_review_asset_pack_preview: 'quote-assetpack-fee', + bitcode_quote_asset_pack_fee: 'settle-assetpack-fee', + bitcode_settle_asset_pack: 'deliver-assetpack-after-finality', + bitcode_deliver_asset_pack: 'delivery-admitted', +}; + +function buildValidatorFromContract(contract: BtdChatGptAppActionContract): z.ZodTypeAny { + const schema = contract.inputSchema as { + properties?: Record; + required?: string[]; + }; + const required = new Set(schema.required ?? []); + const shape: Record = {}; + + for (const [field, fieldSchema] of Object.entries(schema.properties ?? {})) { + let zodField: z.ZodTypeAny; + if (fieldSchema.type === 'boolean') { + zodField = z.boolean(); + } else if (fieldSchema.type === 'integer') { + zodField = z.number().int(); + } else if (fieldSchema.type === 'number') { + zodField = z.number(); + } else { + zodField = z.string().min(1, `${field} must be provided`); + } + shape[field] = required.has(field) ? zodField : zodField.optional(); + } + + return z.object(shape).strict(); +} + +function stableChatGptActionRoot(prefix: string, actionId: BtdChatGptAppActionId, payload: unknown): string { + const hash = createHash('sha256') + .update(stableChatGptJson({ actionId, payload })) + .digest('hex') + .slice(0, 24); + return `${prefix}:${hash}`; +} + +function stableChatGptJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableChatGptJson).join(',')}]`; + if (value && typeof value === 'object') { + return `{${Object.entries(value as Record) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, entry]) => `${JSON.stringify(key)}:${stableChatGptJson(entry)}`) + .join(',')}}`; + } + + return JSON.stringify(value) ?? 'undefined'; +} + +function executeChatGptReadingAction( + contract: BtdChatGptAppActionContract, + args: Record, +): BitcodeToolExecutionResult { + const requestRoot = stableChatGptActionRoot('chatgpt-action-request', contract.actionId, args); + const responseFields: Record = {}; + + for (const field of contract.responseRootFields) { + if (field === 'nextAction') { + responseFields[field] = CHATGPT_READING_ACTION_NEXT_ACTION[contract.actionId]; + } else if (field === 'candidateCount') { + responseFields[field] = Number(args.candidateLimit ?? 0); + } else if (field === 'fitQuality') { + responseFields[field] = 'source-safe-preview-measurements-only'; + } else if (field === 'btcAmountSats') { + responseFields[field] = 'deterministic-fee-quote-pending-measurement-finalization'; + } else if (field === 'finalityState') { + responseFields[field] = args.confirmationState ?? 'pending'; + } else if (field === 'deliveryState') { + responseFields[field] = 'admitted-after-settlement-and-read-license-proof'; + } else if (field === 'reviewState') { + responseFields[field] = args.decision ?? 'reviewed'; + } else if (field === 'readinessState') { + responseFields[field] = 'admitted-for-read-need-comprehension'; + } else if (field.endsWith('Root')) { + responseFields[field] = args[field] ?? stableChatGptActionRoot(`chatgpt-${field}`, contract.actionId, args); + } else { + responseFields[field] = args[field] ?? 'source-safe'; + } + } + + const sourceSafeResponse = renderBtdChatGptAppSourceSafeResponse({ + actionId: contract.actionId, + status: 'accepted', + requestRoot, + visibleFields: responseFields, + proofRootProjection: { + writeAdmission: contract.actionId === 'bitcode_settle_asset_pack' || contract.actionId === 'bitcode_deliver_asset_pack' + ? 'chatgpt-confirmed-action' + : 'not-required-for-source-safe-chatgpt-action', + }, + }); + + return { + result: sourceSafeResponse.visibleFields, + metadata: { + actionContract: { + actionId: contract.actionId, + flowObject: contract.flowObject, + contractRoot: contract.contractRoot, + inputSchemaId: contract.inputSchemaId, + outputSchemaId: contract.outputSchemaId, + authPolicyId: contract.authPolicyId, + sourceSafeRendererId: contract.sourceSafeRendererId, + }, + sourceSafeResponse, + }, + }; +} + +function getChatGptReadingActionTools(): BitcodeTool[] { + const registry = buildBtdChatGptAppActionContractRegistry(); + return registry.actions.map((contract) => ({ + name: contract.actionId, + description: contract.description, + inputSchema: contract.inputSchema, + validator: buildValidatorFromContract(contract), + execute: async (args) => executeChatGptReadingAction(contract, args as Record), + meta: { + readOnlyHint: !['bitcode_settle_asset_pack', 'bitcode_deliver_asset_pack'].includes(contract.actionId), + requiresConfirmation: ['bitcode_settle_asset_pack', 'bitcode_deliver_asset_pack'].includes(contract.actionId), + categories: ['reading', 'source-safe-action'], + contractRoot: contract.contractRoot, + flowObject: contract.flowObject, + sourceSafeRendererId: contract.sourceSafeRendererId, + deniedStates: contract.deniedStates.map((state) => ({ + code: state.code, + readableMessage: state.readableMessage, + repairAction: state.repairAction, + })), + }, + })); +} + // --------------------------------------------------------------------------- // Tool registry // --------------------------------------------------------------------------- export function getBitcodeTools(): BitcodeTool[] { return [ + ...getChatGptReadingActionTools(), { name: 'answer_codebase_query', description: 'Answer technical questions about the codebase by running high-signal searches.', diff --git a/packages/chatgptapp/tsconfig.json b/packages/chatgptapp/tsconfig.json index 0745a5936..a42f96831 100644 --- a/packages/chatgptapp/tsconfig.json +++ b/packages/chatgptapp/tsconfig.json @@ -7,6 +7,7 @@ "types": ["node", "jest"], "paths": { "@bitcode/btd": ["packages/btd/src/authority.ts"], + "@bitcode/btd/chatgpt-app-action-contract": ["packages/btd/src/chatgpt-app-action-contract.ts"], "@bitcode/btd/interface-integration-contract": ["packages/btd/src/interface-integration-contract.ts"], "@bitcode/tools-generics": ["packages/chatgptapp/src/__stubs__/tools-generics.ts"], "@bitcode/generic-tools-simple-system-text-search": ["packages/chatgptapp/src/__stubs__/generic-tools-simple-system-text-search.ts"], diff --git a/packages/chatgptapp/tsconfig.test.json b/packages/chatgptapp/tsconfig.test.json index 3f4782d3c..3a3caa336 100644 --- a/packages/chatgptapp/tsconfig.test.json +++ b/packages/chatgptapp/tsconfig.test.json @@ -7,6 +7,7 @@ "noEmit": true, "paths": { "@bitcode/btd": ["packages/btd/src/authority.ts"], + "@bitcode/btd/chatgpt-app-action-contract": ["packages/btd/src/chatgpt-app-action-contract.ts"], "@bitcode/tools-generics": ["packages/chatgptapp/src/__stubs__/tools-generics.ts"], "@bitcode/generic-tools-simple-system-text-search": ["packages/chatgptapp/src/__stubs__/generic-tools-simple-system-text-search.ts"], "@bitcode/generic-tools-web-search": ["packages/chatgptapp/src/__stubs__/generic-tools-web-search.ts"], diff --git a/packages/protocol/src/canonical/v21-specifying.js b/packages/protocol/src/canonical/v21-specifying.js index d6689cb60..4b337a05d 100644 --- a/packages/protocol/src/canonical/v21-specifying.js +++ b/packages/protocol/src/canonical/v21-specifying.js @@ -361,7 +361,8 @@ function buildV21LikeProfile(version) { ...(version === 'V33' ? [ '.bitcode/v33-interface-contract-catalog.json', - '.bitcode/v33-mcp-api-tool-contracts.json' + '.bitcode/v33-mcp-api-tool-contracts.json', + '.bitcode/v33-chatgpt-app-action-contracts.json' ] : []), ...(version === 'V26' diff --git a/scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs b/scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs new file mode 100644 index 000000000..ee222163d --- /dev/null +++ b/scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs @@ -0,0 +1,320 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { buildV33ChatGptAppActionContractsArtifact } from './generate-v33-chatgpt-app-action-contracts.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const defaultRepoRoot = path.resolve(__dirname, '..'); +const ARTIFACT = '.bitcode/v33-chatgpt-app-action-contracts.json'; + +const REQUIRED_ACTION_IDS = [ + 'bitcode_request_read', + 'bitcode_review_read_need', + 'bitcode_request_finding_fits', + 'bitcode_review_asset_pack_preview', + 'bitcode_quote_asset_pack_fee', + 'bitcode_settle_asset_pack', + 'bitcode_deliver_asset_pack', +]; + +const REQUIRED_FLOW_OBJECTS = [ + 'read_request', + 'read_need', + 'finding_fits_result', + 'asset_pack_preview', + 'btc_fee_quote', + 'settlement_unlock', + 'asset_pack_delivery', +]; + +const REQUIRED_DENIED_STATES = [ + 'SCHEMA_VALIDATION_FAILED', + 'MISSING_READER_SESSION', + 'READ_NEED_REQUIRED', + 'FINDING_FITS_REQUIRED', + 'ASSET_PACK_PREVIEW_REQUIRED', + 'FEE_QUOTE_REQUIRED', + 'SETTLEMENT_REQUIRED', + 'READ_LICENSE_REQUIRED', + 'ORGANIZATION_AUTHORITY_REQUIRED', + 'CONFIRMATION_REQUIRED', +]; + +const REQUIRED_PROOF_ROOT_FIELDS = [ + 'actionId', + 'inputSchemaId', + 'outputSchemaId', + 'authPolicyId', + 'requestRoot', + 'responseRoot', + 'sourceSafeRendererId', + 'writeAdmission', +]; + +const SECRET_MARKERS = [ + `${['sk', 'proj'].join('-')}-`, + `${['sb', 'secret'].join('_')}__`, + ['service', 'role'].join('_'), + ['eyJhbGciOiJI', 'UzI1NiIsInR5cCI6IkpXVCJ9'].join(''), +]; + +function parseArgs(argv) { + const args = { + skipBranchCheck: false, + repoRoot: defaultRepoRoot, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--skip-branch-check') args.skipBranchCheck = true; + else if (arg === '--repo-root') args.repoRoot = path.resolve(argv[++index]); + else if (arg === '--help' || arg === '-h') args.help = true; + else throw new Error(`Unknown argument ${arg}`); + } + + return args; +} + +function read(root, relativePath) { + return readFileSync(path.join(root, relativePath), 'utf8'); +} + +function fileExists(root, relativePath) { + return existsSync(path.join(root, relativePath)); +} + +function git(root, args) { + return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim(); +} + +function run(root, command, args) { + return execFileSync(command, args, { + cwd: root, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); +} + +function assertCheck(failures, condition, message) { + if (!condition) failures.push(message); +} + +function includesAll(values, requiredValues) { + return requiredValues.every((value) => values.includes(value)); +} + +function printHelp() { + process.stdout.write( + [ + 'Usage: node scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs [--skip-branch-check] [--repo-root ]', + '', + 'Checks V33 Gate 4 ChatGPT App action contracts, source-safe rendering, denial repair posture, docs, tests, artifact freshness, and workflow wiring.', + ].join('\n'), + ); + process.stdout.write('\n'); +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + if (args.help) { + printHelp(); + return; + } + + const root = args.repoRoot; + const failures = []; + const pointer = read(root, 'BITCODE_SPEC.txt').trim(); + + assertCheck( + failures, + pointer === 'V32', + `BITCODE_SPEC.txt must remain V32 during V33 gate work. Observed ${pointer || 'empty'}.`, + ); + + if (!args.skipBranchCheck) { + const branch = git(root, ['branch', '--show-current']); + assertCheck( + failures, + branch === 'version/v33' || /^v33\/gate-(?:[4-9]|10)-[a-z0-9][a-z0-9-]*$/u.test(branch), + `V33 Gate 4+ work must occur on version/v33 or v33/gate-4..10-* branches. Observed ${branch || 'detached HEAD'}.`, + ); + } + + const requiredFiles = [ + ARTIFACT, + 'packages/btd/src/chatgpt-app-action-contract.ts', + 'packages/btd/__tests__/chatgpt-app-action-contract.test.ts', + 'packages/chatgptapp/src/tools.ts', + 'packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts', + 'packages/chatgptapp/src/__tests__/tools.test.ts', + 'scripts/generate-v33-chatgpt-app-action-contracts.mjs', + 'scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs', + 'BITCODE_SPEC_V33.md', + 'BITCODE_SPEC_V33_DELTA.md', + 'BITCODE_SPEC_V33_NOTES.md', + 'BITCODE_SPEC_V33_PARITY_MATRIX.md', + 'SPECIFICATIONS_ROADMAP.md', + 'package.json', + '.github/workflows/bitcode-gate-quality.yml', + 'packages/protocol/src/canonical/v21-specifying.js', + ]; + + for (const relativePath of requiredFiles) { + assertCheck(failures, fileExists(root, relativePath), `Missing V33 Gate 4 file: ${relativePath}`); + } + + if (failures.length === 0) { + try { + run(root, 'pnpm', ['run', 'check:v33-chatgpt-app-action-contracts']); + const artifact = JSON.parse(read(root, ARTIFACT)); + const expected = buildV33ChatGptAppActionContractsArtifact(); + assertCheck( + failures, + stableStringify(artifact) === stableStringify(expected), + 'V33 Gate 4 artifact must be generated from the current generator.', + ); + } catch (error) { + failures.push(`V33 Gate 4 artifact generation failed: ${error.stderr || error.message}`); + } + } + + const serializedArtifact = fileExists(root, ARTIFACT) ? read(root, ARTIFACT) : ''; + for (const marker of SECRET_MARKERS) { + assertCheck(failures, !serializedArtifact.includes(marker), `V33 Gate 4 artifact must not contain secret marker ${marker}.`); + } + + const artifact = serializedArtifact ? JSON.parse(serializedArtifact) : null; + if (artifact) { + assertCheck(failures, artifact.artifactId === 'v33-chatgpt-app-action-contracts', 'Gate 4 artifactId must match.'); + assertCheck(failures, artifact.schemaId === 'bitcode.v33.chatGptAppActionContracts.v1', 'Gate 4 schemaId must match.'); + assertCheck(failures, artifact.version === 'V33' && artifact.currentTarget === 'V32', 'Gate 4 artifact must bind V33 over active V32.'); + assertCheck(failures, artifact.passed === true, 'Gate 4 artifact must pass.'); + assertCheck( + failures, + artifact.sourceSafetyVerdict === 'source-safe-chatgpt-app-action-contract-metadata', + 'Gate 4 artifact must be source-safe ChatGPT App metadata.', + ); + assertCheck(failures, includesAll(artifact.requiredActionIds, REQUIRED_ACTION_IDS), 'Gate 4 artifact must enumerate all ChatGPT action ids.'); + assertCheck(failures, includesAll(artifact.requiredFlowObjects, REQUIRED_FLOW_OBJECTS), 'Gate 4 artifact must enumerate all Reading flow objects.'); + assertCheck(failures, includesAll(artifact.requiredDeniedStates, REQUIRED_DENIED_STATES), 'Gate 4 artifact must enumerate denied states.'); + assertCheck(failures, includesAll(artifact.requiredProofRootFields, REQUIRED_PROOF_ROOT_FIELDS), 'Gate 4 artifact must enumerate proof-root fields.'); + assertCheck(failures, artifact.coverage.deniedStateCoverage === true, 'Gate 4 denied-state coverage must pass.'); + assertCheck(failures, artifact.coverage.proofRootCoverage === true, 'Gate 4 proof-root coverage must pass.'); + assertCheck(failures, artifact.coverage.sourceSafeRendererCoverage === true, 'Gate 4 source-safe renderer coverage must pass.'); + assertCheck(failures, artifact.coverage.protectedSourceVisible === false, 'Gate 4 must keep locked contents invisible.'); + assertCheck(failures, artifact.coverage.credentialsSerialized === false, 'Gate 4 artifact must not serialize credentials.'); + assertCheck( + failures, + artifact.contracts.every((contract) => + contract.requiredPermissions.includes('chatgpt.reading.invoke') && + contract.deniedStates.includes('SCHEMA_VALIDATION_FAILED') && + contract.sourceSafeRendererId.startsWith('chatgpt.sourceSafeRenderer.'), + ), + 'Gate 4 contracts must require chatgpt.reading.invoke, schema denials, and source-safe renderers.', + ); + assertCheck( + failures, + artifact.sourceEvidence.every((entry) => entry.requiredTokens.every((token) => token.present === true)), + 'Gate 4 source evidence tokens must all be present.', + ); + assertCheck( + failures, + artifact.testEvidence.every((entry) => entry.requiredTokens.every((token) => token.present === true)), + 'Gate 4 test evidence tokens must all be present.', + ); + } + + const spec = read(root, 'BITCODE_SPEC_V33.md'); + const delta = read(root, 'BITCODE_SPEC_V33_DELTA.md'); + const notes = read(root, 'BITCODE_SPEC_V33_NOTES.md'); + const parity = read(root, 'BITCODE_SPEC_V33_PARITY_MATRIX.md'); + const roadmap = read(root, 'SPECIFICATIONS_ROADMAP.md'); + const packageJson = read(root, 'package.json'); + const workflow = read(root, '.github/workflows/bitcode-gate-quality.yml'); + const btdSource = read(root, 'packages/btd/src/chatgpt-app-action-contract.ts'); + const chatgptSource = read(root, 'packages/chatgptapp/src/tools.ts'); + const btdTest = read(root, 'packages/btd/__tests__/chatgpt-app-action-contract.test.ts'); + const chatgptTest = read(root, 'packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts'); + const specifying = read(root, 'packages/protocol/src/canonical/v21-specifying.js'); + + for (const doc of [spec, delta, notes, parity]) { + assertCheck(failures, doc.includes(ARTIFACT), `V33 docs must mention ${ARTIFACT}.`); + assertCheck(failures, doc.includes('ChatGptAppActionContract'), 'V33 docs must name ChatGptAppActionContract.'); + assertCheck(failures, doc.includes('bitcode_request_finding_fits'), 'V33 docs must name the Finding Fits ChatGPT action.'); + assertCheck(failures, doc.includes('READ_LICENSE_REQUIRED'), 'V33 docs must name read-license denial.'); + } + + assertCheck( + failures, + /Current working gate: V33 Gate (?:[4-9]|10)\b/u.test(roadmap), + 'Roadmap must track V33 Gate 4 or later as the current working gate.', + ); + assertCheck(failures, packageJson.includes('"generate:v33-chatgpt-app-action-contracts"'), 'package.json must expose the Gate 4 generator.'); + assertCheck(failures, packageJson.includes('"check:v33-chatgpt-app-action-contracts"'), 'package.json must expose the Gate 4 artifact check.'); + assertCheck(failures, packageJson.includes('"check:v33-gate4"'), 'package.json must expose check:v33-gate4.'); + assertCheck(failures, workflow.includes('check-v33-gate4-chatgpt-app-action-contracts.mjs'), 'Gate workflow must run the V33 Gate 4 checker.'); + assertCheck(failures, workflow.includes('chatgpt-action-contract.test.ts'), 'Gate workflow must run ChatGPT action contract tests.'); + assertCheck(failures, specifying.includes(ARTIFACT), 'Spec-family profile must include the Gate 4 artifact path.'); + + for (const phrase of [ + 'buildBtdChatGptAppActionContractRegistry', + 'renderBtdChatGptAppSourceSafeResponse', + 'bitcode_request_finding_fits', + 'bitcode_deliver_asset_pack', + 'READ_LICENSE_REQUIRED', + ]) { + assertCheck(failures, btdSource.includes(phrase), `Gate 4 BTD source must include ${phrase}.`); + } + for (const phrase of [ + 'buildBtdChatGptAppActionContractRegistry', + 'renderBtdChatGptAppSourceSafeResponse', + 'getChatGptReadingActionTools', + 'source-safe-action', + ]) { + assertCheck(failures, chatgptSource.includes(phrase), `Gate 4 ChatGPT source must include ${phrase}.`); + } + for (const phrase of [ + 'publishes package-owned contracts for the full Reading action sequence', + 'renders source-safe accepted responses with proof-root projection', + 'renders readable denied responses with repair actions', + ]) { + assertCheck(failures, btdTest.includes(phrase), `Gate 4 BTD test must assert: ${phrase}.`); + } + for (const phrase of [ + 'registers every package-owned Reading action contract as a ChatGPT App tool', + 'requires schema-valid arguments before executing package-owned actions', + 'renders denied states with readable repair actions for ChatGPT App responses', + ]) { + assertCheck(failures, chatgptTest.includes(phrase), `Gate 4 ChatGPT test must assert: ${phrase}.`); + } + + if (failures.length) { + process.stderr.write('V33 Gate 4 ChatGPT App action contract check failed:\n'); + for (const failure of failures) { + process.stderr.write(`- ${failure}\n`); + } + process.exit(1); + } + + process.stdout.write(`V33 Gate 4 ChatGPT App action contracts ok artifact=${ARTIFACT}\n`); +} + +function stableStringify(value) { + return JSON.stringify(sortJson(value)); +} + +function sortJson(value) { + if (Array.isArray(value)) return value.map(sortJson); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => [key, sortJson(entry)]), + ); +} + +main(); diff --git a/scripts/generate-v33-chatgpt-app-action-contracts.mjs b/scripts/generate-v33-chatgpt-app-action-contracts.mjs new file mode 100644 index 000000000..60be63258 --- /dev/null +++ b/scripts/generate-v33-chatgpt-app-action-contracts.mjs @@ -0,0 +1,304 @@ +#!/usr/bin/env node + +import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const ARTIFACT_PATH = '.bitcode/v33-chatgpt-app-action-contracts.json'; +const GENERATED_AT = '2026-05-22T00:00:00.000Z'; + +const secretMarkers = Object.freeze([ + `${['sk', 'proj'].join('-')}-`, + `${['sb', 'secret'].join('_')}__`, + ['service', 'role'].join('_'), + ['eyJhbGciOiJI', 'UzI1NiIsInR5cCI6IkpXVCJ9'].join(''), + ['SUPABASE', 'SERVICE', 'ROLE'].join('_'), + ['OPENAI', 'API', 'KEY'].join('_'), + ['VERCEL', 'TOKEN'].join('_'), + ['VERCEL', 'OIDC', 'TOKEN'].join('_'), +]); +const secretPattern = new RegExp(secretMarkers.map(escapeRegex).join('|'), 'u'); + +const requiredActionIds = Object.freeze([ + 'bitcode_request_read', + 'bitcode_review_read_need', + 'bitcode_request_finding_fits', + 'bitcode_review_asset_pack_preview', + 'bitcode_quote_asset_pack_fee', + 'bitcode_settle_asset_pack', + 'bitcode_deliver_asset_pack', +]); + +const requiredFlowObjects = Object.freeze([ + 'read_request', + 'read_need', + 'finding_fits_result', + 'asset_pack_preview', + 'btc_fee_quote', + 'settlement_unlock', + 'asset_pack_delivery', +]); + +const requiredProofRootFields = Object.freeze([ + 'actionId', + 'inputSchemaId', + 'outputSchemaId', + 'authPolicyId', + 'requestRoot', + 'responseRoot', + 'sourceSafeRendererId', + 'writeAdmission', +]); + +const requiredDeniedStates = Object.freeze([ + 'SCHEMA_VALIDATION_FAILED', + 'MISSING_READER_SESSION', + 'READ_NEED_REQUIRED', + 'FINDING_FITS_REQUIRED', + 'ASSET_PACK_PREVIEW_REQUIRED', + 'FEE_QUOTE_REQUIRED', + 'SETTLEMENT_REQUIRED', + 'READ_LICENSE_REQUIRED', + 'ORGANIZATION_AUTHORITY_REQUIRED', + 'CONFIRMATION_REQUIRED', +]); + +const actionContracts = Object.freeze([ + action('bitcode_request_read', 'read_request', ['readerId', 'repository', 'readRequest'], ['requestId', 'interfaceSessionId', 'attachmentsRoot'], ['readRequestRoot', 'readinessState', 'nextAction'], ['SCHEMA_VALIDATION_FAILED', 'MISSING_READER_SESSION']), + action('bitcode_review_read_need', 'read_need', ['readRequestRoot', 'readNeedRoot', 'decision'], ['feedback'], ['acceptedNeedRoot', 'reviewState', 'nextAction'], ['SCHEMA_VALIDATION_FAILED', 'MISSING_READER_SESSION', 'READ_NEED_REQUIRED']), + action('bitcode_request_finding_fits', 'finding_fits_result', ['acceptedNeedRoot', 'depositoryScope'], ['minimumFitConfidence', 'candidateLimit'], ['findingFitsResultRoot', 'candidateCount', 'nextAction'], ['SCHEMA_VALIDATION_FAILED', 'MISSING_READER_SESSION', 'READ_NEED_REQUIRED']), + action('bitcode_review_asset_pack_preview', 'asset_pack_preview', ['findingFitsResultRoot', 'assetPackPreviewRoot'], ['previewDecision'], ['assetPackPreviewRoot', 'fitQuality', 'nextAction'], ['SCHEMA_VALIDATION_FAILED', 'MISSING_READER_SESSION', 'FINDING_FITS_REQUIRED', 'ASSET_PACK_PREVIEW_REQUIRED']), + action('bitcode_quote_asset_pack_fee', 'btc_fee_quote', ['assetPackPreviewRoot', 'measurementVectorRoot'], ['feePolicyId'], ['feeQuoteRoot', 'btcAmountSats', 'nextAction'], ['SCHEMA_VALIDATION_FAILED', 'MISSING_READER_SESSION', 'ASSET_PACK_PREVIEW_REQUIRED']), + action('bitcode_settle_asset_pack', 'settlement_unlock', ['feeQuoteRoot', 'walletId', 'confirmationState', 'confirmed'], ['btcPaymentIntentId'], ['settlementRoot', 'finalityState', 'nextAction'], ['SCHEMA_VALIDATION_FAILED', 'MISSING_READER_SESSION', 'FEE_QUOTE_REQUIRED', 'CONFIRMATION_REQUIRED']), + action('bitcode_deliver_asset_pack', 'asset_pack_delivery', ['settlementRoot', 'readLicenseId', 'deliveryTarget', 'confirmed'], ['organizationAuthorityRoot'], ['deliveryRoot', 'deliveryState', 'nextAction'], ['SCHEMA_VALIDATION_FAILED', 'MISSING_READER_SESSION', 'SETTLEMENT_REQUIRED', 'READ_LICENSE_REQUIRED', 'ORGANIZATION_AUTHORITY_REQUIRED', 'CONFIRMATION_REQUIRED'], 'locked-assetpack-delivery'), +]); + +const sourceFiles = Object.freeze([ + 'packages/btd/src/chatgpt-app-action-contract.ts', + 'packages/btd/src/index.ts', + 'packages/chatgptapp/src/tools.ts', +]); + +const testFiles = Object.freeze([ + 'packages/btd/__tests__/chatgpt-app-action-contract.test.ts', + 'packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts', + 'packages/chatgptapp/src/__tests__/tools.test.ts', + 'scripts/check-v33-gate4-chatgpt-app-action-contracts.mjs', +]); + +function action(actionId, flowObject, requestRootFields, optionalRequestRootFields, responseRootFields, deniedStates, sourceSafetyClass = 'source-safe-internal') { + return { + actionId, + flowObject, + inputSchemaId: `bitcode.chatgpt.${actionId}.input.v1`, + outputSchemaId: `bitcode.chatgpt.${actionId}.output.v1`, + authPolicyId: 'interface.authorization.chatgpt-reading-action', + requiredPermissions: ['chatgpt.reading.invoke'], + sourceSafetyClass, + sourceSafeRendererId: `chatgpt.sourceSafeRenderer.${actionId}`, + requestRootFields: [...requestRootFields, ...optionalRequestRootFields], + responseRootFields, + proofRootFields: requiredProofRootFields, + deniedStates, + examples: [ + `${actionId}-success-source-safe`, + `${actionId}-denied-readable-repair`, + ], + validationCommands: [ + 'pnpm --filter @bitcode/btd test -- --runTestsByPath __tests__/chatgpt-app-action-contract.test.ts', + 'pnpm --dir packages/chatgptapp exec jest --runTestsByPath src/__tests__/chatgpt-action-contract.test.ts src/__tests__/tools.test.ts --runInBand', + ], + }; +} + +function escapeRegex(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); +} + +function read(relativePath) { + return readFileSync(path.join(repoRoot, relativePath), 'utf8'); +} + +function sha256(value) { + return `sha256:${createHash('sha256').update(value).digest('hex')}`; +} + +function stableRoot(prefix, parts) { + const hash = createHash('sha256').update(parts.join('\u001f')).digest('hex').slice(0, 24); + return `${prefix}:${hash}`; +} + +function sortJson(value) { + if (Array.isArray(value)) return value.map(sortJson); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => [key, sortJson(entry)]), + ); +} + +function stableStringify(value) { + return `${JSON.stringify(sortJson(value), null, 2)}\n`; +} + +function scanTokens(relativePath, tokens) { + const text = read(relativePath); + return { + relativePath, + digest: sha256(text), + requiredTokens: tokens.map((token) => ({ + token, + present: text.includes(token), + })), + }; +} + +function allTokensPresent(scan) { + return scan.requiredTokens.every((entry) => entry.present); +} + +function withContractRoots(contracts) { + return contracts.map((contract) => ({ + ...contract, + contractRoot: stableRoot('v33-chatgpt-app-action-contract', [ + contract.actionId, + contract.flowObject, + contract.inputSchemaId, + contract.outputSchemaId, + contract.authPolicyId, + contract.requiredPermissions.join(','), + contract.sourceSafetyClass, + contract.sourceSafeRendererId, + contract.requestRootFields.join(','), + contract.responseRootFields.join(','), + contract.proofRootFields.join(','), + contract.deniedStates.join(','), + contract.examples.join(','), + ]), + })); +} + +export function buildV33ChatGptAppActionContractsArtifact() { + const contracts = withContractRoots(actionContracts); + const observedActionIds = contracts.map((contract) => contract.actionId); + const observedFlowObjects = contracts.map((contract) => contract.flowObject); + const missingActionIds = requiredActionIds.filter((actionId) => !observedActionIds.includes(actionId)); + const missingFlowObjects = requiredFlowObjects.filter((flowObject) => !observedFlowObjects.includes(flowObject)); + const sourceEvidence = [ + scanTokens('packages/btd/src/chatgpt-app-action-contract.ts', [ + 'buildBtdChatGptAppActionContractRegistry', + 'renderBtdChatGptAppSourceSafeResponse', + 'bitcode_request_finding_fits', + 'bitcode_deliver_asset_pack', + 'SCHEMA_VALIDATION_FAILED', + 'READ_LICENSE_REQUIRED', + ]), + scanTokens('packages/chatgptapp/src/tools.ts', [ + 'buildBtdChatGptAppActionContractRegistry', + 'renderBtdChatGptAppSourceSafeResponse', + 'getChatGptReadingActionTools', + 'source-safe-action', + ]), + ]; + const testEvidence = [ + scanTokens('packages/btd/__tests__/chatgpt-app-action-contract.test.ts', [ + 'publishes package-owned contracts for the full Reading action sequence', + 'renders source-safe accepted responses with proof-root projection', + 'renders readable denied responses with repair actions', + ]), + scanTokens('packages/chatgptapp/src/__tests__/chatgpt-action-contract.test.ts', [ + 'registers every package-owned Reading action contract as a ChatGPT App tool', + 'requires schema-valid arguments before executing package-owned actions', + 'renders denied states with readable repair actions for ChatGPT App responses', + ]), + ]; + const sourceEvidenceComplete = sourceEvidence.every(allTokensPresent); + const testEvidenceComplete = testEvidence.every(allTokensPresent); + const deniedStateCoverage = requiredDeniedStates.every((state) => + contracts.some((contract) => contract.deniedStates.includes(state)), + ); + const proofRootCoverage = requiredProofRootFields.every((field) => + contracts.every((contract) => contract.proofRootFields.includes(field)), + ); + const passed = + missingActionIds.length === 0 && + missingFlowObjects.length === 0 && + deniedStateCoverage && + proofRootCoverage && + sourceEvidenceComplete && + testEvidenceComplete; + + return { + artifactId: 'v33-chatgpt-app-action-contracts', + schemaId: 'bitcode.v33.chatGptAppActionContracts.v1', + version: 'V33', + currentTarget: 'V32', + generatedAt: GENERATED_AT, + sourceSafetyVerdict: 'source-safe-chatgpt-app-action-contract-metadata', + requiredActionIds, + requiredFlowObjects, + requiredProofRootFields, + requiredDeniedStates, + contracts, + coverage: { + observedActionIds, + missingActionIds, + observedFlowObjects, + missingFlowObjects, + deniedStateCoverage, + proofRootCoverage, + sourceSafeRendererCoverage: contracts.every((contract) => contract.sourceSafeRendererId.startsWith('chatgpt.sourceSafeRenderer.')), + protectedSourceVisible: false, + credentialsSerialized: false, + sourceEvidenceComplete, + testEvidenceComplete, + }, + sourceEvidence, + testEvidence, + sourceFiles: sourceFiles.map((relativePath) => ({ + relativePath, + digest: sha256(read(relativePath)), + })), + testFiles: testFiles.map((relativePath) => ({ + relativePath, + digest: sha256(read(relativePath)), + })), + artifactRoot: stableRoot('v33-chatgpt-app-action-contracts-artifact', [ + contracts.map((contract) => contract.contractRoot).join(','), + String(passed), + ]), + passed, + }; +} + +function main() { + const checkOnly = process.argv.includes('--check'); + const artifact = buildV33ChatGptAppActionContractsArtifact(); + const serialized = stableStringify(artifact); + + if (secretPattern.test(serialized)) { + throw new Error(`${ARTIFACT_PATH} would contain a secret marker.`); + } + + if (checkOnly) { + const current = read(ARTIFACT_PATH); + if (current !== serialized) { + throw new Error(`${ARTIFACT_PATH} is stale. Run pnpm run generate:v33-chatgpt-app-action-contracts.`); + } + process.stdout.write(`${ARTIFACT_PATH} is current\n`); + return; + } + + mkdirSync(path.dirname(path.join(repoRoot, ARTIFACT_PATH)), { recursive: true }); + writeFileSync(path.join(repoRoot, ARTIFACT_PATH), serialized); + process.stdout.write(`${ARTIFACT_PATH}\n`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +}