@atmem/architect 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONSTITUTION.md +144 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +31 -2
- package/agent-skills/discovery/SKILL.md +48 -0
- package/agent-skills/discovery/topics.json +139 -0
- package/agent-skills/generation-capabilities.json +760 -0
- package/agent-skills/service-lifecycle.json +21 -0
- package/agent-skills/specialists/architecture/SKILL.md +15 -0
- package/agent-skills/specialists/aws/SKILL.md +17 -0
- package/agent-skills/specialists/azure/SKILL.md +7 -0
- package/agent-skills/specialists/composition/SKILL.md +17 -0
- package/agent-skills/specialists/devops/SKILL.md +10 -0
- package/agent-skills/specialists/finops/SKILL.md +7 -0
- package/agent-skills/specialists/gcp/SKILL.md +7 -0
- package/agent-skills/specialists/ibm/SKILL.md +7 -0
- package/agent-skills/specialists/manifest.json +13 -0
- package/agent-skills/specialists/pulumi/SKILL.md +7 -0
- package/agent-skills/specialists/security/SKILL.md +7 -0
- package/agent-skills/specialists/terraform/SKILL.md +14 -0
- package/agent-skills/upstream/aws/LICENSE +175 -0
- package/agent-skills/upstream/aws/SKILL.md +125 -0
- package/agent-skills/upstream/azure/LICENSE +21 -0
- package/agent-skills/upstream/azure/SKILL.md +387 -0
- package/agent-skills/upstream/gcp/LICENSE +202 -0
- package/agent-skills/upstream/gcp/SKILL.md +159 -0
- package/agent-skills/upstream/manifest.json +57 -0
- package/agent-skills/upstream/pulumi/LICENSE +201 -0
- package/agent-skills/upstream/pulumi/SKILL.md +501 -0
- package/agent-skills/upstream/terraform/LICENSE +373 -0
- package/agent-skills/upstream/terraform/SKILL.md +316 -0
- package/agent-skills/workflows/workflows.json +601 -0
- package/cli/architect.mjs +3 -0
- package/cli/runtime.mjs +115 -0
- package/dist/assets/html2canvas.esm-QH1iLAAe.js +22 -0
- package/dist/assets/index-BTLHaa3J.css +1 -0
- package/dist/assets/index-CQ2C1gKF.js +21338 -0
- package/dist/assets/index.es-DTyqIdaO.js +18 -0
- package/dist/assets/purify.es-CAi2OJFm.js +3 -0
- package/dist/assets/visual-export-Ox0j5xj5.js +173 -0
- package/dist/favicon.svg +2 -0
- package/dist/fonts/DM-Sans-OFL.txt +93 -0
- package/dist/fonts/Manrope-OFL.txt +93 -0
- package/dist/fonts/NotoSans-Regular.ttf +0 -0
- package/dist/fonts/OFL.txt +94 -0
- package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/index.html +4 -0
- package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
- package/dist/licenses/aws-architect-LICENSE.txt +17 -0
- package/dist/licenses/aws-mcp-LICENSE.txt +175 -0
- package/dist/licenses/aws-mcp-NOTICE.txt +1 -0
- package/dist/licenses/cloud-emulation-notices.txt +36619 -0
- package/dist/licenses/dependency-notices.txt +6379 -0
- package/dist/licenses/emulation-notices.txt +717 -0
- package/dist/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/dist/licenses/memory-dependency-notices.txt +3319 -0
- package/dist/licenses/openarchflow-LICENSE.txt +21 -0
- package/dist/licenses/python-dependency-notices.txt +11951 -0
- package/docs/upstream-lock.json +26 -0
- package/knowledge/engineering-practices.json +536 -0
- package/package.json +30 -4
- package/packaging/uv-release-lock.json +54 -0
- package/public/licenses/atmem-APACHE-2.0.txt +202 -0
- package/public/licenses/aws-architect-LICENSE.txt +17 -0
- package/public/licenses/aws-mcp-LICENSE.txt +175 -0
- package/public/licenses/aws-mcp-NOTICE.txt +1 -0
- package/public/licenses/cloud-emulation-notices.txt +36619 -0
- package/public/licenses/dependency-notices.txt +6379 -0
- package/public/licenses/emulation-notices.txt +717 -0
- package/public/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/public/licenses/memory-dependency-notices.txt +3319 -0
- package/public/licenses/openarchflow-LICENSE.txt +21 -0
- package/public/licenses/python-dependency-notices.txt +11951 -0
- package/scripts/terraform-render.mjs +8 -0
- package/server/__init__.py +0 -0
- package/server/abstraction.py +28 -0
- package/server/agent_runtime.py +153 -0
- package/server/app.py +257 -0
- package/server/architect.py +112 -0
- package/server/architecture_activity.py +67 -0
- package/server/attachments.py +101 -0
- package/server/cloud-emulation-requirements.txt +29 -0
- package/server/codex_app_server.py +219 -0
- package/server/codex_engineering_backend.py +266 -0
- package/server/codex_worker_policy.py +193 -0
- package/server/contracts.py +315 -0
- package/server/copilot_worker.py +16 -0
- package/server/dependency_mirror.py +54 -0
- package/server/design_patch.py +43 -0
- package/server/emulation-requirements.txt +8 -0
- package/server/emulation_adapters.py +20 -0
- package/server/emulation_routes.py +98 -0
- package/server/emulation_worker.py +351 -0
- package/server/engineering_artifacts.py +221 -0
- package/server/engineering_assets/application-runtime.json +4372 -0
- package/server/engineering_assets/architecture_work.schema.json +79 -0
- package/server/engineering_assets/materialize_runtime.py +62 -0
- package/server/engineering_assets/prepare_tools.py +96 -0
- package/server/engineering_assets/sql_validation.py +59 -0
- package/server/engineering_contexts.py +120 -0
- package/server/engineering_delivery.py +210 -0
- package/server/engineering_finalization.py +210 -0
- package/server/engineering_handoff.py +172 -0
- package/server/engineering_jobs.py +206 -0
- package/server/engineering_knowledge.py +75 -0
- package/server/engineering_reconciliation.py +159 -0
- package/server/engineering_routes.py +483 -0
- package/server/engineering_sessions.py +273 -0
- package/server/engineering_tools.py +166 -0
- package/server/generation_lint.py +18 -0
- package/server/local_providers.py +185 -0
- package/server/local_validation.py +236 -0
- package/server/memory-requirements.lock +181 -0
- package/server/memory-requirements.txt +6 -0
- package/server/memory_context.py +58 -0
- package/server/memory_routes.py +141 -0
- package/server/memory_service.py +306 -0
- package/server/memory_worker.py +129 -0
- package/server/openai_voice.py +80 -0
- package/server/orca_handoff.py +90 -0
- package/server/packaged_app.py +9 -0
- package/server/pricing.py +190 -0
- package/server/process_control_observation.py +31 -0
- package/server/prompts.py +136 -0
- package/server/public_faq_emulation_worker.py +44 -0
- package/server/requirements.lock +1618 -0
- package/server/requirements.txt +68 -0
- package/server/runtime_dependency_locks.py +27 -0
- package/server/runtime_evidence.py +93 -0
- package/server/settings.py +14 -0
- package/server/skills.py +103 -0
- package/server/storage_emulation_worker.py +160 -0
- package/server/structured_emulation_worker.py +48 -0
- package/server/terraform_routes.py +118 -0
- package/server/upstream/__init__.py +0 -0
- package/server/upstream/agent/__init__.py +1 -0
- package/server/upstream/agent/aws_architect.py +600 -0
- package/server/upstream/agent/config/__init__.py +1 -0
- package/server/upstream/agent/config/agent_config.py +82 -0
- package/server/upstream/agent/config/prompts.py +138 -0
- package/server/upstream/agent/rate_limiter.py +62 -0
- package/server/upstream/utils/__init__.py +1 -0
- package/server/upstream/utils/file_writer.py +43 -0
- package/server/validation_applicability.py +85 -0
- package/server/validation_jobs.py +252 -0
- package/server/validation_workers.py +48 -0
- package/server/voice_routes.py +91 -0
- package/server/voice_worker.py +26 -0
- package/server/windows_snapshot.py +119 -0
- package/specs/028-convergence-evaluation/evidence/20261110-ibm-invoice-native/registry-structured/qualification.json +72 -0
- package/specs/028-convergence-evaluation/evidence/20261110-invoice-runtime/durable-current/receipt.json +61 -0
- package/specs/029-general-purpose-engine/evidence/20261111-runtime-dependencies/sdk/receipt.json +22 -0
- package/src/ArchitectureActivity.jsx +54 -0
- package/src/ArchitectureWorkPanel.jsx +39 -0
- package/src/ArtifactChecks.jsx +49 -0
- package/src/BlockRequirements.jsx +13 -0
- package/src/BrainActivity.jsx +11 -0
- package/src/ChatMessage.jsx +29 -0
- package/src/ChatResizeHandle.jsx +6 -0
- package/src/CodePreview.jsx +8 -0
- package/src/ComponentReview.jsx +31 -0
- package/src/DeliveryViews.jsx +59 -0
- package/src/EmulationPanel.jsx +44 -0
- package/src/EngagementViews.jsx +387 -0
- package/src/EngineeringPanel.jsx +151 -0
- package/src/FileTree.jsx +7 -0
- package/src/FinalChecklist.jsx +15 -0
- package/src/HelpNote.jsx +3 -0
- package/src/ImplementationDelivery.jsx +91 -0
- package/src/JourneyViews.jsx +403 -0
- package/src/LocationChoices.jsx +12 -0
- package/src/MemoryPanel.jsx +58 -0
- package/src/OpenAIVoicePanel.jsx +66 -0
- package/src/OutputsView.jsx +71 -0
- package/src/ProjectChecks.jsx +44 -0
- package/src/TerraformView.jsx +57 -0
- package/src/WorkbenchDialogs.jsx +532 -0
- package/src/activity-presentation.mjs +28 -0
- package/src/ai-qualification-scenarios.mjs +31 -0
- package/src/approval-application-files.mjs +32 -0
- package/src/approval-applications.mjs +82 -0
- package/src/approval-spec-kit.mjs +30 -0
- package/src/architecture-activity.css +28 -0
- package/src/architecture-activity.mjs +63 -0
- package/src/architecture-work.mjs +176 -0
- package/src/artifact-work.mjs +103 -0
- package/src/async-documents.mjs +191 -0
- package/src/atflow.css +1140 -0
- package/src/atforma.css +748 -0
- package/src/atmem-tokens.css +79 -0
- package/src/aws-preflight.mjs +60 -0
- package/src/aws-public-faq.mjs +102 -0
- package/src/aws-validation-scenarios.mjs +10 -0
- package/src/aws-validation.mjs +48 -0
- package/src/azure-plan-mocks.mjs +25 -0
- package/src/azure-structured.mjs +133 -0
- package/src/bedrock-summary.mjs +152 -0
- package/src/bounded-rag.mjs +60 -0
- package/src/brand.css +9 -0
- package/src/canvas-view.mjs +28 -0
- package/src/capability-composition.mjs +44 -0
- package/src/chat-attachments.mjs +13 -0
- package/src/cloud-provider-locks.mjs +9 -0
- package/src/cloud-storage-scenarios.mjs +2 -0
- package/src/component-review.mjs +55 -0
- package/src/composition-spec-kit.mjs +30 -0
- package/src/consultation-context.mjs +15 -0
- package/src/consultation.mjs +52 -0
- package/src/container-contracts.mjs +5 -0
- package/src/container-inspection.mjs +75 -0
- package/src/container-runtime.mjs +30 -0
- package/src/container-security-inspection.mjs +49 -0
- package/src/container-terraform.mjs +109 -0
- package/src/conversation.mjs +18 -0
- package/src/core.mjs +863 -0
- package/src/cross-cloud-storage.mjs +100 -0
- package/src/delivery-capabilities.mjs +25 -0
- package/src/delivery.mjs +58 -0
- package/src/deployment-ownership.mjs +10 -0
- package/src/deployment-region.mjs +11 -0
- package/src/derive.mjs +31 -0
- package/src/design-build-flow.mjs +10 -0
- package/src/diagram-attention.mjs +24 -0
- package/src/discovery.mjs +359 -0
- package/src/document.mjs +170 -0
- package/src/emulation-feedback.mjs +61 -0
- package/src/emulation-guidance.mjs +37 -0
- package/src/engagement.mjs +274 -0
- package/src/engineering-client.mjs +73 -0
- package/src/engineering-defaults.mjs +17 -0
- package/src/engineering.css +31 -0
- package/src/enterprise-application.mjs +76 -0
- package/src/enterprise-aws-host-infrastructure.mjs +62 -0
- package/src/enterprise-aws-host-tests.mjs +15 -0
- package/src/enterprise-aws-infrastructure-tests.mjs +38 -0
- package/src/enterprise-aws-infrastructure.mjs +138 -0
- package/src/enterprise-google-infrastructure.mjs +84 -0
- package/src/enterprise-google-tests.mjs +25 -0
- package/src/estimate.mjs +190 -0
- package/src/existing-references.mjs +18 -0
- package/src/external-identities.mjs +17 -0
- package/src/faq-lambda-build.mjs +28 -0
- package/src/final-package.mjs +34 -0
- package/src/gcp-structured.mjs +104 -0
- package/src/generation-provider.mjs +7 -0
- package/src/generation-quality.mjs +69 -0
- package/src/handoff.mjs +141 -0
- package/src/ibm-registry.mjs +12 -0
- package/src/ibm-structured.mjs +103 -0
- package/src/identifiers.mjs +15 -0
- package/src/implementation-map.mjs +14 -0
- package/src/implementation-outcome.mjs +8 -0
- package/src/incomplete-terraform.mjs +9 -0
- package/src/integrations.ts +137 -0
- package/src/interface-contracts.mjs +30 -0
- package/src/invoice-application.mjs +66 -0
- package/src/invoice-aws-operations.mjs +136 -0
- package/src/invoice-aws.mjs +143 -0
- package/src/invoice-azure-operations.mjs +147 -0
- package/src/invoice-azure.mjs +134 -0
- package/src/invoice-builder.mjs +35 -0
- package/src/invoice-dependencies.mjs +43 -0
- package/src/invoice-gcp-operations.mjs +178 -0
- package/src/invoice-gcp.mjs +104 -0
- package/src/invoice-ibm-operations.mjs +278 -0
- package/src/invoice-ibm.mjs +142 -0
- package/src/invoice-operations.mjs +143 -0
- package/src/invoice-plan.mjs +45 -0
- package/src/invoice-region.mjs +6 -0
- package/src/invoice-spec-kit.mjs +61 -0
- package/src/invoice-state.mjs +7 -0
- package/src/invoice-terraform.mjs +29 -0
- package/src/invoice-tests.mjs +122 -0
- package/src/journey-workspace.css +106 -0
- package/src/journey.mjs +97 -0
- package/src/levels.mjs +145 -0
- package/src/local-agent.mjs +184 -0
- package/src/main.jsx +2481 -0
- package/src/model-policy-preflight.mjs +34 -0
- package/src/openai-voice.mjs +102 -0
- package/src/output-bundle.mjs +13 -0
- package/src/ownership-inspection.mjs +28 -0
- package/src/pack.mjs +201 -0
- package/src/pricing.mjs +85 -0
- package/src/production-state.mjs +8 -0
- package/src/project-copy.mjs +10 -0
- package/src/project-file.mjs +160 -0
- package/src/project-links.mjs +3 -0
- package/src/protected-state.mjs +43 -0
- package/src/public-faq-builder.mjs +19 -0
- package/src/public-faq-clouds.mjs +251 -0
- package/src/public-faq.mjs +122 -0
- package/src/pulumi.mjs +123 -0
- package/src/python-container.mjs +25 -0
- package/src/python-runtime.mjs +50 -0
- package/src/rag-handler.mjs +43 -0
- package/src/rag-web-assets.mjs +52 -0
- package/src/rag-web.mjs +68 -0
- package/src/records.mjs +177 -0
- package/src/resources.mjs +163 -0
- package/src/review.mjs +252 -0
- package/src/runtime/EnterpriseDocument.java +89 -0
- package/src/runtime/approved_http_destination.py +72 -0
- package/src/runtime/aws-lambda-qualification-lock.json +7 -0
- package/src/runtime/aws-python-lock.json +82 -0
- package/src/runtime/aws-web-python-lock.json +887 -0
- package/src/runtime/aws_json_model.py +42 -0
- package/src/runtime/azure-python-lock.json +863 -0
- package/src/runtime/azure-sources.json +8 -0
- package/src/runtime/azure-test-lock.json +23 -0
- package/src/runtime/azure_acceptance.py +192 -0
- package/src/runtime/azure_structured.py +153 -0
- package/src/runtime/bounded_rag.py +126 -0
- package/src/runtime/catalog_admin.py +70 -0
- package/src/runtime/cloud-sql-proxy-lock.json +8 -0
- package/src/runtime/cloud-sql-proxy-security-review.json +30 -0
- package/src/runtime/connector_cli.py +50 -0
- package/src/runtime/connector_sync.py +70 -0
- package/src/runtime/durable-python-lock.json +247 -0
- package/src/runtime/durable_workflow.py +325 -0
- package/src/runtime/enterprise-application-manifest.json +110 -0
- package/src/runtime/enterprise-application-sources.json +266 -0
- package/src/runtime/enterprise-auth-python-lock.json +592 -0
- package/src/runtime/enterprise-aws-host-bootstrap-source.json +1 -0
- package/src/runtime/enterprise-aws-host-probe-source.json +1 -0
- package/src/runtime/enterprise-aws-task-template.json +235 -0
- package/src/runtime/enterprise-document-lock.json +18 -0
- package/src/runtime/enterprise-document.Dockerfile +15 -0
- package/src/runtime/enterprise-runtime-aws-python-lock.json +1115 -0
- package/src/runtime/enterprise-runtime-azure-python-lock.json +1106 -0
- package/src/runtime/enterprise-runtime-gcp-python-lock.json +1060 -0
- package/src/runtime/enterprise-runtime-ibm-python-lock.json +1096 -0
- package/src/runtime/enterprise-runtime-python-lock.json +1058 -0
- package/src/runtime/enterprise-runtime.Dockerfile +15 -0
- package/src/runtime/enterprise-web-app.js +1 -0
- package/src/runtime/enterprise-web-entry.js +105 -0
- package/src/runtime/enterprise-web-lock.json +29 -0
- package/src/runtime/enterprise-web-notices.txt +229 -0
- package/src/runtime/enterprise-web.css +4 -0
- package/src/runtime/enterprise-web.html +8 -0
- package/src/runtime/enterprise_audit.py +91 -0
- package/src/runtime/enterprise_audit_schema.sql +25 -0
- package/src/runtime/enterprise_auth_schema.sql +67 -0
- package/src/runtime/enterprise_auth_store.py +241 -0
- package/src/runtime/enterprise_aws_host_bootstrap.py +120 -0
- package/src/runtime/enterprise_aws_host_probe.py +105 -0
- package/src/runtime/enterprise_aws_hosts.py +136 -0
- package/src/runtime/enterprise_aws_jobs.py +300 -0
- package/src/runtime/enterprise_aws_objects.py +93 -0
- package/src/runtime/enterprise_aws_transfer.py +83 -0
- package/src/runtime/enterprise_azure_batch_api.py +128 -0
- package/src/runtime/enterprise_azure_host.py +599 -0
- package/src/runtime/enterprise_azure_jobs.py +294 -0
- package/src/runtime/enterprise_azure_objects.py +234 -0
- package/src/runtime/enterprise_azure_pool.py +161 -0
- package/src/runtime/enterprise_azure_transfer.py +122 -0
- package/src/runtime/enterprise_configuration.py +136 -0
- package/src/runtime/enterprise_document.py +116 -0
- package/src/runtime/enterprise_document_cli.py +32 -0
- package/src/runtime/enterprise_document_contract.py +54 -0
- package/src/runtime/enterprise_document_job.py +109 -0
- package/src/runtime/enterprise_document_jobs.py +122 -0
- package/src/runtime/enterprise_document_jobs_schema.sql +23 -0
- package/src/runtime/enterprise_extraction_cache.py +186 -0
- package/src/runtime/enterprise_extraction_schema.sql +17 -0
- package/src/runtime/enterprise_google_jobs.py +275 -0
- package/src/runtime/enterprise_google_transfer.py +85 -0
- package/src/runtime/enterprise_http.py +212 -0
- package/src/runtime/enterprise_identity.py +52 -0
- package/src/runtime/enterprise_knowledge.py +152 -0
- package/src/runtime/enterprise_models.py +94 -0
- package/src/runtime/enterprise_native.py +181 -0
- package/src/runtime/enterprise_oauth.py +132 -0
- package/src/runtime/enterprise_operator.py +160 -0
- package/src/runtime/enterprise_parser_service.py +136 -0
- package/src/runtime/enterprise_parser_transport.py +68 -0
- package/src/runtime/enterprise_queries.py +255 -0
- package/src/runtime/enterprise_queries_schema.sql +27 -0
- package/src/runtime/enterprise_query_service.py +111 -0
- package/src/runtime/enterprise_secrets.py +110 -0
- package/src/runtime/enterprise_sessions.py +95 -0
- package/src/runtime/enterprise_sources.py +260 -0
- package/src/runtime/enterprise_web.py +39 -0
- package/src/runtime/enterprise_worker.py +68 -0
- package/src/runtime/export_contract_acceptance.py +18 -0
- package/src/runtime/faq-output-contract.json +40 -0
- package/src/runtime/faq-web-app.js +41 -0
- package/src/runtime/faq-web.css +1 -0
- package/src/runtime/faq-web.html +6 -0
- package/src/runtime/gcp-sources.json +9 -0
- package/src/runtime/gcp_acceptance.py +110 -0
- package/src/runtime/gcp_structured.py +150 -0
- package/src/runtime/http_acceptance.py +39 -0
- package/src/runtime/iap-web-app.js +36 -0
- package/src/runtime/iap-web-source.json +1 -0
- package/src/runtime/ibm-python-lock.json +856 -0
- package/src/runtime/ibm-sources.json +9 -0
- package/src/runtime/ibm_acceptance.py +133 -0
- package/src/runtime/ibm_structured.py +113 -0
- package/src/runtime/invoice-azapi-provider-lock.json +7 -0
- package/src/runtime/invoice-ibm-provider-lock.json +7 -0
- package/src/runtime/invoice-parser-lock.json +1199 -0
- package/src/runtime/invoice-parser-python-lock.json +56 -0
- package/src/runtime/invoice-parser.Dockerfile +190 -0
- package/src/runtime/invoice-sources.json +58 -0
- package/src/runtime/invoice-test-sources.json +24 -0
- package/src/runtime/invoice-web.css +2 -0
- package/src/runtime/invoice-web.html +12 -0
- package/src/runtime/invoice-web.js +112 -0
- package/src/runtime/invoice_configuration.py +17 -0
- package/src/runtime/invoice_document.py +95 -0
- package/src/runtime/invoice_extraction.py +96 -0
- package/src/runtime/invoice_http.py +129 -0
- package/src/runtime/invoice_identity.py +5 -0
- package/src/runtime/invoice_maintenance.py +40 -0
- package/src/runtime/invoice_native.py +77 -0
- package/src/runtime/invoice_object_store.py +153 -0
- package/src/runtime/invoice_operations.py +108 -0
- package/src/runtime/invoice_operator.py +79 -0
- package/src/runtime/invoice_operator_core.py +39 -0
- package/src/runtime/invoice_operator_job.py +147 -0
- package/src/runtime/invoice_parser_cli.py +17 -0
- package/src/runtime/invoice_parser_identity.py +39 -0
- package/src/runtime/invoice_parser_service.py +89 -0
- package/src/runtime/invoice_parser_transport.py +89 -0
- package/src/runtime/invoice_processing.py +24 -0
- package/src/runtime/invoice_retention.py +16 -0
- package/src/runtime/invoice_runtime_secrets.py +43 -0
- package/src/runtime/invoice_stage.py +80 -0
- package/src/runtime/invoice_web.py +39 -0
- package/src/runtime/invoice_worker.py +50 -0
- package/src/runtime/lambda_faq.py +5 -0
- package/src/runtime/parser-requirements.lock +4 -0
- package/src/runtime/policy_review.py +18 -0
- package/src/runtime/postgres-qualification-lock.json +21 -0
- package/src/runtime/public-faq-sources.json +22 -0
- package/src/runtime/public_catalog_admin.py +122 -0
- package/src/runtime/public_catalog_store.py +125 -0
- package/src/runtime/public_faq.py +144 -0
- package/src/runtime/public_faq_app.py +49 -0
- package/src/runtime/python-container-lock.json +18 -0
- package/src/runtime/rag-sources.json +10 -0
- package/src/runtime/rag_acceptance.py +60 -0
- package/src/runtime/rag_aws_adapter.py +95 -0
- package/src/runtime/rds-ca-source.json +9 -0
- package/src/runtime/rds-global-ca.pem +2888 -0
- package/src/runtime/state-contract-check-source.json +4 -0
- package/src/runtime/state_contract_check.py +338 -0
- package/src/runtime/structured-source.json +1 -0
- package/src/runtime/structured-web-app.js +71 -0
- package/src/runtime/structured-web-source.json +1 -0
- package/src/runtime/structured_http.py +105 -0
- package/src/runtime/structured_output.py +48 -0
- package/src/runtime/web-python-lock.json +802 -0
- package/src/runtime/web_user_admin.py +44 -0
- package/src/runtime/workflow_application.py +62 -0
- package/src/runtime/workflow_bindings.py +128 -0
- package/src/runtime/workflow_configuration.py +40 -0
- package/src/runtime/workflow_connection.py +105 -0
- package/src/runtime/workflow_contract.py +54 -0
- package/src/runtime/workflow_database.py +94 -0
- package/src/runtime/workflow_errors.py +6 -0
- package/src/runtime/workflow_http.py +178 -0
- package/src/runtime/workflow_identity.py +69 -0
- package/src/runtime/workflow_payload.py +118 -0
- package/src/runtime/workflow_process_stage.py +88 -0
- package/src/runtime/workflow_profile.py +91 -0
- package/src/runtime/workflow_review.py +106 -0
- package/src/runtime/workflow_role_bootstrap.py +104 -0
- package/src/runtime/workflow_schema.sql +72 -0
- package/src/runtime/workflow_worker.py +142 -0
- package/src/runtime-settings.mjs +15 -0
- package/src/s3-access.mjs +14 -0
- package/src/source-contracts.mjs +17 -0
- package/src/spec-kit.mjs +186 -0
- package/src/state-custody.mjs +32 -0
- package/src/state-inspection.mjs +45 -0
- package/src/state-spec-kit.mjs +13 -0
- package/src/storage-tests.mjs +25 -0
- package/src/structured-api.mjs +133 -0
- package/src/structured-contract.mjs +19 -0
- package/src/structured-operation-policy.mjs +2 -0
- package/src/structured-web.mjs +17 -0
- package/src/style.css +1 -0
- package/src/system-description.mjs +26 -0
- package/src/terraform-admission.mjs +66 -0
- package/src/terraform-composition.mjs +83 -0
- package/src/terraform-expressions.mjs +46 -0
- package/src/terraform-validation-coverage.mjs +11 -0
- package/src/terraform.mjs +128 -0
- package/src/text-statistics.mjs +51 -0
- package/src/upstream/openarchflow/format-payload.ts +26 -0
- package/src/upstream/openarchflow/import/mermaid.ts +205 -0
- package/src/upstream/openarchflow/import/sql-ddl.ts +109 -0
- package/src/upstream/openarchflow/layout.ts +54 -0
- package/src/upstream/openarchflow/providers/aws.ts +196 -0
- package/src/upstream/openarchflow/providers/azure.ts +126 -0
- package/src/upstream/openarchflow/providers/cloudnative.ts +71 -0
- package/src/upstream/openarchflow/providers/generic.ts +227 -0
- package/src/upstream/openarchflow/providers/integration.ts +22 -0
- package/src/upstream/openarchflow/providers/types.ts +2 -0
- package/src/upstream/openarchflow/templates.ts +937 -0
- package/src/upstream-catalog.mjs +10 -0
- package/src/useChatScroll.jsx +37 -0
- package/src/useEmulation.jsx +36 -0
- package/src/useEngineeringStatus.mjs +17 -0
- package/src/validation-receipt.mjs +10 -0
- package/src/visual-export.mjs +92 -0
- package/src/voice-conversation.mjs +117 -0
- package/src/work-intake.mjs +24 -0
- package/src/workflow-runtime-inventory.mjs +3 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": "\"\"\"Local synthetic HTTP-state protocol check; never opens a project backend.\n\nThis fixture is not a production state service. All state, credentials, databases\nand native workspaces are created in one fresh directory. No apply, migration,\nprovider, cloud credential or caller-supplied endpoint is used.\n\"\"\"\nimport argparse\nimport base64\nimport contextlib\nimport hashlib\nimport http.server\nimport json\nimport os\nfrom pathlib import Path\nimport secrets\nimport shutil\nimport sqlite3\nimport subprocess\nimport threading\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nimport uuid\n\n\ndef encoded(value):\n return json.dumps(value, sort_keys=True, separators=(',', ':')).encode()\n\n\ndef digest(value):\n return hashlib.sha256(value).hexdigest()\n\n\nclass FixtureStore:\n def __init__(self, path, initial):\n self.path = path\n with self.connection() as db:\n db.executescript('''CREATE TABLE IF NOT EXISTS versions(\n id INTEGER PRIMARY KEY, payload BLOB NOT NULL, sha TEXT NOT NULL);\n CREATE TABLE IF NOT EXISTS head(id INTEGER PRIMARY KEY CHECK(id=1),version INTEGER NOT NULL);\n CREATE TABLE IF NOT EXISTS lease(id INTEGER PRIMARY KEY CHECK(id=1),actor TEXT,info TEXT);''')\n if not db.execute('SELECT 1 FROM head').fetchone():\n db.execute('INSERT INTO versions VALUES(1,?,?)', (initial, digest(initial)))\n db.execute('INSERT INTO head VALUES(1,1)')\n\n @contextlib.contextmanager\n def connection(self):\n db = sqlite3.connect(self.path, timeout=5)\n try:\n db.execute('PRAGMA journal_mode=WAL')\n db.execute('PRAGMA synchronous=FULL')\n db.execute('BEGIN IMMEDIATE')\n yield db\n db.commit()\n except BaseException:\n db.rollback()\n raise\n finally:\n db.close()\n\n def read(self):\n with self.connection() as db:\n row = db.execute('SELECT payload,sha FROM versions JOIN head ON versions.id=head.version').fetchone()\n if digest(row[0]) != row[1]:\n raise ValueError('Fixture state digest mismatch')\n return row[0]\n\n def lock(self):\n with self.connection() as db:\n row = db.execute('SELECT actor,info FROM lease').fetchone()\n return (row[0], json.loads(row[1])) if row else None\n\n def acquire(self, actor, info):\n if not isinstance(info.get('ID'), str) or not info['ID'] or len(info['ID']) > 100:\n return 400, {}\n with self.connection() as db:\n row = db.execute('SELECT actor,info FROM lease').fetchone()\n if row:\n return 409, json.loads(row[1])\n db.execute('INSERT INTO lease VALUES(1,?,?)', (actor, json.dumps(info)))\n return 200, {}\n\n def unlock(self, actor, info):\n with self.connection() as db:\n row = db.execute('SELECT actor,info FROM lease').fetchone()\n if not row:\n return 409, {}\n current = json.loads(row[1])\n if actor != row[0] or info.get('ID') != current['ID']:\n return 409, current\n db.execute('DELETE FROM lease')\n return 200, {}\n\n def denied_write(self, actor, lock_id):\n with self.connection() as db:\n row = db.execute('SELECT actor,info FROM lease').fetchone()\n # Fixture intentionally has no HTTP write implementation. Plans and\n # state pull must not write; only the test controller adds versions.\n if not row or row[0] != actor or json.loads(row[1])['ID'] != lock_id:\n return 409\n return 405\n\n def add_fixture_version(self, payload):\n with self.connection() as db:\n if db.execute('SELECT 1 FROM lease').fetchone():\n raise ValueError('An active writer forbids fixture version changes')\n cursor = db.execute('INSERT INTO versions(payload,sha) VALUES(?,?)', (payload, digest(payload)))\n db.execute('UPDATE head SET version=?', (cursor.lastrowid,))\n return cursor.lastrowid\n\n def restore_fixture_version(self, version):\n with self.connection() as db:\n if db.execute('SELECT 1 FROM lease').fetchone():\n raise ValueError('An active writer forbids fixture recovery')\n row = db.execute('SELECT payload,sha FROM versions WHERE id=?', (version,)).fetchone()\n if not row or digest(row[0]) != row[1]:\n raise ValueError('Unverified fixture backup')\n json.loads(row[0])\n db.execute('UPDATE head SET version=?', (version,))\n\n\nclass Control:\n def __init__(self):\n self.pause = False\n self.read_held = threading.Event()\n self.release = threading.Event()\n self.events = []\n\n\ndef fixture_server(store, credentials, control):\n class Handler(http.server.BaseHTTPRequestHandler):\n def log_message(self, *_):\n pass\n\n def respond(self, status, value=None):\n body = value if isinstance(value, bytes) else encoded(value or {})\n self.send_response(status)\n self.send_header('Content-Type', 'application/json')\n self.send_header('Content-Length', str(len(body)))\n self.end_headers()\n try:\n self.wfile.write(body)\n except (BrokenPipeError, ConnectionResetError):\n pass\n\n def actor(self):\n header = self.headers.get('Authorization', '')\n for actor, password in credentials.items():\n expected = 'Basic ' + base64.b64encode((actor + ':' + password).encode()).decode()\n if secrets.compare_digest(header, expected):\n return actor\n return None\n\n def dispatch(self):\n actor = self.actor()\n route = urllib.parse.urlsplit(self.path)\n if not actor:\n self.respond(401)\n return\n control.events.append({'method': self.command, 'path': route.path, 'actor': actor})\n if self.command == 'GET' and route.path == '/state':\n lease = store.lock()\n if control.pause and lease and lease[0] == actor:\n control.read_held.set()\n if not control.release.wait(25):\n self.respond(503)\n return\n self.respond(200, store.read())\n return\n length = int(self.headers.get('Content-Length', '0'))\n if length < 0 or length > 65536:\n self.respond(413)\n return\n try:\n value = json.loads(self.rfile.read(length)) if length else {}\n except ValueError:\n self.respond(400)\n return\n if not isinstance(value, dict):\n self.respond(400)\n return\n if route.path == '/lock' and self.command in {'POST', 'DELETE'}:\n status, response = (store.acquire if self.command == 'POST' else store.unlock)(actor, value)\n self.respond(status, response)\n elif route.path == '/state' and self.command == 'POST':\n lock_id = urllib.parse.parse_qs(route.query).get('ID', [None])[0]\n self.respond(store.denied_write(actor, lock_id))\n else:\n self.respond(405)\n\n do_GET = dispatch\n do_POST = dispatch\n do_DELETE = dispatch\n\n server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)\n server.daemon_threads = True\n return server\n\n\ndef run_probe(terraform, folder):\n folder.mkdir(parents=True, exist_ok=False)\n private = folder / 'private'\n private.mkdir(mode=0o700)\n os.chmod(folder, 0o700)\n credentials = {actor: secrets.token_urlsafe(32) for actor in ['writer-a', 'writer-b']}\n base_env = {'PATH': str(terraform.parent), 'HOME': str(private), 'TMPDIR': str(private),\n 'TF_IN_AUTOMATION': '1', 'CHECKPOINT_DISABLE': '1', 'TF_INPUT': '0',\n 'TF_HTTP_RETRY_MAX': '0', 'AWS_EC2_METADATA_DISABLED': 'true'}\n cli = private / 'empty.tfrc'\n cli.write_text('provider_installation { filesystem_mirror { path = ' + json.dumps(str(private)) + ' } }\\n')\n base_env['TF_CLI_CONFIG_FILE'] = str(cli)\n version = json.loads(subprocess.check_output([str(terraform), 'version', '-json'], env=base_env, timeout=15))\n initial = encoded({'version': 4, 'terraform_version': version['terraform_version'], 'serial': 1,\n 'lineage': str(uuid.uuid4()), 'outputs': {'fixture': {'value': 'synthetic-state', 'type': 'string'}}, 'resources': []})\n store = FixtureStore(private / 'fixture.sqlite', initial)\n control = Control()\n server = fixture_server(store, credentials, control)\n base = 'http://127.0.0.1:' + str(server.server_port)\n thread = threading.Thread(target=server.serve_forever, daemon=True)\n thread.start()\n checks, commands = [], []\n active = None\n\n def check(name, condition):\n checks.append({'name': name, 'passed': bool(condition)})\n if not condition:\n raise AssertionError(name)\n\n def environment(actor):\n return {**base_env, 'TF_HTTP_USERNAME': actor, 'TF_HTTP_PASSWORD': credentials[actor]}\n\n def native(actor, arguments, expected_exit=0):\n result = subprocess.run([str(terraform), *arguments], cwd=private / actor,\n env=environment(actor), capture_output=True, text=True, timeout=20)\n commands.append({'actor': actor, 'arguments': arguments, 'exitCode': result.returncode, 'expectedExitCode': expected_exit})\n (private / ('command-' + str(len(commands)) + '.log')).write_text(result.stdout + '\\n' + result.stderr)\n return result\n\n def request(actor, method, path, value=None, password=None):\n header = base64.b64encode((actor + ':' + (password or credentials[actor])).encode()).decode()\n req = urllib.request.Request(base + path, data=encoded(value) if value is not None else None,\n method=method, headers={'Authorization': 'Basic ' + header})\n opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))\n try:\n with opener.open(req, timeout=5) as response:\n return response.status\n except urllib.error.HTTPError as error:\n return error.code\n\n try:\n for actor in credentials:\n workspace = private / actor\n workspace.mkdir()\n (workspace / 'main.tf.json').write_text(json.dumps({'terraform': {'required_version': '>= 1.10.0, < 2.0.0', 'backend': {'http': {}}}, 'output': {'fixture': {'value': 'synthetic-state'}}}))\n (workspace / 'backend.hcl').write_text('address = ' + json.dumps(base + '/state') + '\\nlock_address = ' + json.dumps(base + '/lock') + '\\nunlock_address = ' + json.dumps(base + '/lock') + '\\nlock_method = \"POST\"\\nunlock_method = \"DELETE\"\\n')\n check('native init ' + actor, native(actor, ['init', '-input=false', '-no-color', '-backend-config=backend.hcl']).returncode == 0)\n pulled = native('writer-a', ['state', 'pull'])\n check('native reads synthetic lineage and payload', pulled.returncode == 0 and all(json.loads(pulled.stdout).get(k) == json.loads(initial)[k] for k in ['version', 'serial', 'lineage', 'outputs', 'resources']))\n check('unknown credential denied', request('writer-a', 'GET', '/state', password='invalid-fixture-password') == 401)\n control.pause = True\n active = subprocess.Popen([str(terraform), 'plan', '-input=false', '-no-color', '-lock-timeout=0s'], cwd=private / 'writer-a', env=environment('writer-a'), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n check('real native plan holds lock while reading', control.read_held.wait(12) and active.poll() is None and store.lock() is not None)\n current = store.lock()[1]\n competing = native('writer-b', ['plan', '-input=false', '-no-color', '-lock-timeout=0s'], expected_exit=1)\n check('second native writer excluded', competing.returncode == 1 and 'Error acquiring the state lock' in competing.stderr + competing.stdout)\n check('foreign actor cannot unlock owner', request('writer-b', 'DELETE', '/lock', {'ID': current['ID']}) == 409 and store.lock()[1]['ID'] == current['ID'])\n check('incorrect lock ID cannot unlock owner', request('writer-a', 'DELETE', '/lock', {'ID': 'wrong-fixture-lock'}) == 409 and store.lock()[1]['ID'] == current['ID'])\n check('foreign writer cannot replace state', request('writer-b', 'POST', '/state?ID=' + current['ID'], {}) == 409 and store.read() == initial)\n try:\n store.restore_fixture_version(1)\n refused = False\n except ValueError:\n refused = True\n check('recovery refuses an active writer', refused)\n control.pause = False\n control.release.set()\n output, _ = active.communicate(timeout=15)\n (private / 'writer-a-plan.log').write_text(output)\n commands.append({'actor': 'writer-a', 'arguments': ['plan', '-input=false', '-no-color', '-lock-timeout=0s'], 'exitCode': active.returncode, 'expectedExitCode': 0})\n check('native plan succeeds and releases owned lock', active.returncode == 0 and store.lock() is None)\n check('plan-only clients never write state', not any(e['method'] == 'POST' and e['path'] == '/state' and e['actor'] == 'writer-a' for e in control.events) and store.read() == initial)\n changed = json.loads(initial)\n changed['serial'] = 2\n changed['outputs']['fixture']['value'] = 'synthetic-new-version'\n changed_id = store.add_fixture_version(encoded(changed))\n reopened = FixtureStore(private / 'fixture.sqlite', b'not-a-new-seed')\n check('local fixture versions survive reopen', json.loads(reopened.read())['serial'] == 2)\n reopened.restore_fixture_version(1)\n check('verified fixture recovery restores exact bytes', reopened.read() == initial)\n restored = native('writer-b', ['state', 'pull'])\n check('native observes restored lineage and payload', restored.returncode == 0 and json.loads(restored.stdout) == json.loads(pulled.stdout))\n with reopened.connection() as db:\n db.execute('UPDATE versions SET payload=? WHERE id=?', (b'corrupted-synthetic-backup', changed_id))\n try:\n reopened.restore_fixture_version(changed_id)\n refused = False\n except ValueError:\n refused = True\n check('corrupt fixture backup cannot replace good state', refused and reopened.read() == initial)\n abandoned = {'ID': str(uuid.uuid4()), 'Operation': 'synthetic-abandoned-writer'}\n reopened.acquire('writer-a', abandoned)\n restarted = FixtureStore(private / 'fixture.sqlite', b'not-a-new-seed')\n check('uncertain abandoned lock persists across reopen', restarted.lock()[1]['ID'] == abandoned['ID'])\n check('abandoned lock still needs matching owner and ID', restarted.unlock('writer-b', abandoned)[0] == 409 and restarted.unlock('writer-a', abandoned)[0] == 200)\n finally:\n control.pause = False\n control.release.set()\n if active is not None and active.poll() is None:\n active.terminate()\n try:\n active.communicate(timeout=5)\n except subprocess.TimeoutExpired:\n active.kill()\n active.communicate(timeout=5)\n server.shutdown()\n server.server_close()\n thread.join(timeout=5)\n report = {'passed': all(c['passed'] for c in checks), 'scope': 'isolated-loopback-http-state-protocol',\n 'sourceSha256': digest(Path(__file__).read_bytes()), 'terraformVersion': version['terraform_version'],\n 'terraformSha256': digest(terraform.read_bytes()), 'checks': checks, 'commands': commands,\n 'initialSyntheticStateSha256': digest(initial), 'selectedBackendVerified': False,\n 'migrationAuthorized': False, 'boundary': 'Actual Terraform init/plan/state pull against a synthetic loopback HTTP fixture and local SQLite version/lock store. No apply, project backend, TLS, cloud authentication, S3/Azure/GCS behavior, migration or application-system qualification.'}\n serialized = json.dumps(report, indent=2) + '\\n'\n if any(secret in serialized for secret in credentials.values()):\n raise ValueError('Credential appeared in public fixture evidence')\n (folder / 'report.json').write_text(serialized)\n return report\n\n\nif __name__ == '__main__':\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument('--terraform', type=Path, default=shutil.which('terraform'))\n parser.add_argument('--work-dir', type=Path, required=True)\n args = parser.parse_args()\n if not args.terraform:\n parser.error('Install a local Terraform executable or provide --terraform.')\n result = run_probe(Path(args.terraform).resolve(strict=True), args.work_dir.resolve())\n print(json.dumps({'passed': result['passed'], 'checks': len(result['checks']), 'report': str(args.work_dir / 'report.json'), 'selectedBackendVerified': False}))\n",
|
|
3
|
+
"sha256": "c82017b8ef1cf30113d0e0bd0c28268b975cf54ab2208261b7c5610b57a96b28"
|
|
4
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Local synthetic HTTP-state protocol check; never opens a project backend.
|
|
2
|
+
|
|
3
|
+
This fixture is not a production state service. All state, credentials, databases
|
|
4
|
+
and native workspaces are created in one fresh directory. No apply, migration,
|
|
5
|
+
provider, cloud credential or caller-supplied endpoint is used.
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import base64
|
|
9
|
+
import contextlib
|
|
10
|
+
import hashlib
|
|
11
|
+
import http.server
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import secrets
|
|
16
|
+
import shutil
|
|
17
|
+
import sqlite3
|
|
18
|
+
import subprocess
|
|
19
|
+
import threading
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.parse
|
|
22
|
+
import urllib.request
|
|
23
|
+
import uuid
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def encoded(value):
|
|
27
|
+
return json.dumps(value, sort_keys=True, separators=(',', ':')).encode()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def digest(value):
|
|
31
|
+
return hashlib.sha256(value).hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FixtureStore:
|
|
35
|
+
def __init__(self, path, initial):
|
|
36
|
+
self.path = path
|
|
37
|
+
with self.connection() as db:
|
|
38
|
+
db.executescript('''CREATE TABLE IF NOT EXISTS versions(
|
|
39
|
+
id INTEGER PRIMARY KEY, payload BLOB NOT NULL, sha TEXT NOT NULL);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS head(id INTEGER PRIMARY KEY CHECK(id=1),version INTEGER NOT NULL);
|
|
41
|
+
CREATE TABLE IF NOT EXISTS lease(id INTEGER PRIMARY KEY CHECK(id=1),actor TEXT,info TEXT);''')
|
|
42
|
+
if not db.execute('SELECT 1 FROM head').fetchone():
|
|
43
|
+
db.execute('INSERT INTO versions VALUES(1,?,?)', (initial, digest(initial)))
|
|
44
|
+
db.execute('INSERT INTO head VALUES(1,1)')
|
|
45
|
+
|
|
46
|
+
@contextlib.contextmanager
|
|
47
|
+
def connection(self):
|
|
48
|
+
db = sqlite3.connect(self.path, timeout=5)
|
|
49
|
+
try:
|
|
50
|
+
db.execute('PRAGMA journal_mode=WAL')
|
|
51
|
+
db.execute('PRAGMA synchronous=FULL')
|
|
52
|
+
db.execute('BEGIN IMMEDIATE')
|
|
53
|
+
yield db
|
|
54
|
+
db.commit()
|
|
55
|
+
except BaseException:
|
|
56
|
+
db.rollback()
|
|
57
|
+
raise
|
|
58
|
+
finally:
|
|
59
|
+
db.close()
|
|
60
|
+
|
|
61
|
+
def read(self):
|
|
62
|
+
with self.connection() as db:
|
|
63
|
+
row = db.execute('SELECT payload,sha FROM versions JOIN head ON versions.id=head.version').fetchone()
|
|
64
|
+
if digest(row[0]) != row[1]:
|
|
65
|
+
raise ValueError('Fixture state digest mismatch')
|
|
66
|
+
return row[0]
|
|
67
|
+
|
|
68
|
+
def lock(self):
|
|
69
|
+
with self.connection() as db:
|
|
70
|
+
row = db.execute('SELECT actor,info FROM lease').fetchone()
|
|
71
|
+
return (row[0], json.loads(row[1])) if row else None
|
|
72
|
+
|
|
73
|
+
def acquire(self, actor, info):
|
|
74
|
+
if not isinstance(info.get('ID'), str) or not info['ID'] or len(info['ID']) > 100:
|
|
75
|
+
return 400, {}
|
|
76
|
+
with self.connection() as db:
|
|
77
|
+
row = db.execute('SELECT actor,info FROM lease').fetchone()
|
|
78
|
+
if row:
|
|
79
|
+
return 409, json.loads(row[1])
|
|
80
|
+
db.execute('INSERT INTO lease VALUES(1,?,?)', (actor, json.dumps(info)))
|
|
81
|
+
return 200, {}
|
|
82
|
+
|
|
83
|
+
def unlock(self, actor, info):
|
|
84
|
+
with self.connection() as db:
|
|
85
|
+
row = db.execute('SELECT actor,info FROM lease').fetchone()
|
|
86
|
+
if not row:
|
|
87
|
+
return 409, {}
|
|
88
|
+
current = json.loads(row[1])
|
|
89
|
+
if actor != row[0] or info.get('ID') != current['ID']:
|
|
90
|
+
return 409, current
|
|
91
|
+
db.execute('DELETE FROM lease')
|
|
92
|
+
return 200, {}
|
|
93
|
+
|
|
94
|
+
def denied_write(self, actor, lock_id):
|
|
95
|
+
with self.connection() as db:
|
|
96
|
+
row = db.execute('SELECT actor,info FROM lease').fetchone()
|
|
97
|
+
# Fixture intentionally has no HTTP write implementation. Plans and
|
|
98
|
+
# state pull must not write; only the test controller adds versions.
|
|
99
|
+
if not row or row[0] != actor or json.loads(row[1])['ID'] != lock_id:
|
|
100
|
+
return 409
|
|
101
|
+
return 405
|
|
102
|
+
|
|
103
|
+
def add_fixture_version(self, payload):
|
|
104
|
+
with self.connection() as db:
|
|
105
|
+
if db.execute('SELECT 1 FROM lease').fetchone():
|
|
106
|
+
raise ValueError('An active writer forbids fixture version changes')
|
|
107
|
+
cursor = db.execute('INSERT INTO versions(payload,sha) VALUES(?,?)', (payload, digest(payload)))
|
|
108
|
+
db.execute('UPDATE head SET version=?', (cursor.lastrowid,))
|
|
109
|
+
return cursor.lastrowid
|
|
110
|
+
|
|
111
|
+
def restore_fixture_version(self, version):
|
|
112
|
+
with self.connection() as db:
|
|
113
|
+
if db.execute('SELECT 1 FROM lease').fetchone():
|
|
114
|
+
raise ValueError('An active writer forbids fixture recovery')
|
|
115
|
+
row = db.execute('SELECT payload,sha FROM versions WHERE id=?', (version,)).fetchone()
|
|
116
|
+
if not row or digest(row[0]) != row[1]:
|
|
117
|
+
raise ValueError('Unverified fixture backup')
|
|
118
|
+
json.loads(row[0])
|
|
119
|
+
db.execute('UPDATE head SET version=?', (version,))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Control:
|
|
123
|
+
def __init__(self):
|
|
124
|
+
self.pause = False
|
|
125
|
+
self.read_held = threading.Event()
|
|
126
|
+
self.release = threading.Event()
|
|
127
|
+
self.events = []
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def fixture_server(store, credentials, control):
|
|
131
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
132
|
+
def log_message(self, *_):
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def respond(self, status, value=None):
|
|
136
|
+
body = value if isinstance(value, bytes) else encoded(value or {})
|
|
137
|
+
self.send_response(status)
|
|
138
|
+
self.send_header('Content-Type', 'application/json')
|
|
139
|
+
self.send_header('Content-Length', str(len(body)))
|
|
140
|
+
self.end_headers()
|
|
141
|
+
try:
|
|
142
|
+
self.wfile.write(body)
|
|
143
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def actor(self):
|
|
147
|
+
header = self.headers.get('Authorization', '')
|
|
148
|
+
for actor, password in credentials.items():
|
|
149
|
+
expected = 'Basic ' + base64.b64encode((actor + ':' + password).encode()).decode()
|
|
150
|
+
if secrets.compare_digest(header, expected):
|
|
151
|
+
return actor
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def dispatch(self):
|
|
155
|
+
actor = self.actor()
|
|
156
|
+
route = urllib.parse.urlsplit(self.path)
|
|
157
|
+
if not actor:
|
|
158
|
+
self.respond(401)
|
|
159
|
+
return
|
|
160
|
+
control.events.append({'method': self.command, 'path': route.path, 'actor': actor})
|
|
161
|
+
if self.command == 'GET' and route.path == '/state':
|
|
162
|
+
lease = store.lock()
|
|
163
|
+
if control.pause and lease and lease[0] == actor:
|
|
164
|
+
control.read_held.set()
|
|
165
|
+
if not control.release.wait(25):
|
|
166
|
+
self.respond(503)
|
|
167
|
+
return
|
|
168
|
+
self.respond(200, store.read())
|
|
169
|
+
return
|
|
170
|
+
length = int(self.headers.get('Content-Length', '0'))
|
|
171
|
+
if length < 0 or length > 65536:
|
|
172
|
+
self.respond(413)
|
|
173
|
+
return
|
|
174
|
+
try:
|
|
175
|
+
value = json.loads(self.rfile.read(length)) if length else {}
|
|
176
|
+
except ValueError:
|
|
177
|
+
self.respond(400)
|
|
178
|
+
return
|
|
179
|
+
if not isinstance(value, dict):
|
|
180
|
+
self.respond(400)
|
|
181
|
+
return
|
|
182
|
+
if route.path == '/lock' and self.command in {'POST', 'DELETE'}:
|
|
183
|
+
status, response = (store.acquire if self.command == 'POST' else store.unlock)(actor, value)
|
|
184
|
+
self.respond(status, response)
|
|
185
|
+
elif route.path == '/state' and self.command == 'POST':
|
|
186
|
+
lock_id = urllib.parse.parse_qs(route.query).get('ID', [None])[0]
|
|
187
|
+
self.respond(store.denied_write(actor, lock_id))
|
|
188
|
+
else:
|
|
189
|
+
self.respond(405)
|
|
190
|
+
|
|
191
|
+
do_GET = dispatch
|
|
192
|
+
do_POST = dispatch
|
|
193
|
+
do_DELETE = dispatch
|
|
194
|
+
|
|
195
|
+
server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
|
196
|
+
server.daemon_threads = True
|
|
197
|
+
return server
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def run_probe(terraform, folder):
|
|
201
|
+
folder.mkdir(parents=True, exist_ok=False)
|
|
202
|
+
private = folder / 'private'
|
|
203
|
+
private.mkdir(mode=0o700)
|
|
204
|
+
os.chmod(folder, 0o700)
|
|
205
|
+
credentials = {actor: secrets.token_urlsafe(32) for actor in ['writer-a', 'writer-b']}
|
|
206
|
+
base_env = {'PATH': str(terraform.parent), 'HOME': str(private), 'TMPDIR': str(private),
|
|
207
|
+
'TF_IN_AUTOMATION': '1', 'CHECKPOINT_DISABLE': '1', 'TF_INPUT': '0',
|
|
208
|
+
'TF_HTTP_RETRY_MAX': '0', 'AWS_EC2_METADATA_DISABLED': 'true'}
|
|
209
|
+
cli = private / 'empty.tfrc'
|
|
210
|
+
cli.write_text('provider_installation { filesystem_mirror { path = ' + json.dumps(str(private)) + ' } }\n')
|
|
211
|
+
base_env['TF_CLI_CONFIG_FILE'] = str(cli)
|
|
212
|
+
version = json.loads(subprocess.check_output([str(terraform), 'version', '-json'], env=base_env, timeout=15))
|
|
213
|
+
initial = encoded({'version': 4, 'terraform_version': version['terraform_version'], 'serial': 1,
|
|
214
|
+
'lineage': str(uuid.uuid4()), 'outputs': {'fixture': {'value': 'synthetic-state', 'type': 'string'}}, 'resources': []})
|
|
215
|
+
store = FixtureStore(private / 'fixture.sqlite', initial)
|
|
216
|
+
control = Control()
|
|
217
|
+
server = fixture_server(store, credentials, control)
|
|
218
|
+
base = 'http://127.0.0.1:' + str(server.server_port)
|
|
219
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
220
|
+
thread.start()
|
|
221
|
+
checks, commands = [], []
|
|
222
|
+
active = None
|
|
223
|
+
|
|
224
|
+
def check(name, condition):
|
|
225
|
+
checks.append({'name': name, 'passed': bool(condition)})
|
|
226
|
+
if not condition:
|
|
227
|
+
raise AssertionError(name)
|
|
228
|
+
|
|
229
|
+
def environment(actor):
|
|
230
|
+
return {**base_env, 'TF_HTTP_USERNAME': actor, 'TF_HTTP_PASSWORD': credentials[actor]}
|
|
231
|
+
|
|
232
|
+
def native(actor, arguments, expected_exit=0):
|
|
233
|
+
result = subprocess.run([str(terraform), *arguments], cwd=private / actor,
|
|
234
|
+
env=environment(actor), capture_output=True, text=True, timeout=20)
|
|
235
|
+
commands.append({'actor': actor, 'arguments': arguments, 'exitCode': result.returncode, 'expectedExitCode': expected_exit})
|
|
236
|
+
(private / ('command-' + str(len(commands)) + '.log')).write_text(result.stdout + '\n' + result.stderr)
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
def request(actor, method, path, value=None, password=None):
|
|
240
|
+
header = base64.b64encode((actor + ':' + (password or credentials[actor])).encode()).decode()
|
|
241
|
+
req = urllib.request.Request(base + path, data=encoded(value) if value is not None else None,
|
|
242
|
+
method=method, headers={'Authorization': 'Basic ' + header})
|
|
243
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
244
|
+
try:
|
|
245
|
+
with opener.open(req, timeout=5) as response:
|
|
246
|
+
return response.status
|
|
247
|
+
except urllib.error.HTTPError as error:
|
|
248
|
+
return error.code
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
for actor in credentials:
|
|
252
|
+
workspace = private / actor
|
|
253
|
+
workspace.mkdir()
|
|
254
|
+
(workspace / 'main.tf.json').write_text(json.dumps({'terraform': {'required_version': '>= 1.10.0, < 2.0.0', 'backend': {'http': {}}}, 'output': {'fixture': {'value': 'synthetic-state'}}}))
|
|
255
|
+
(workspace / 'backend.hcl').write_text('address = ' + json.dumps(base + '/state') + '\nlock_address = ' + json.dumps(base + '/lock') + '\nunlock_address = ' + json.dumps(base + '/lock') + '\nlock_method = "POST"\nunlock_method = "DELETE"\n')
|
|
256
|
+
check('native init ' + actor, native(actor, ['init', '-input=false', '-no-color', '-backend-config=backend.hcl']).returncode == 0)
|
|
257
|
+
pulled = native('writer-a', ['state', 'pull'])
|
|
258
|
+
check('native reads synthetic lineage and payload', pulled.returncode == 0 and all(json.loads(pulled.stdout).get(k) == json.loads(initial)[k] for k in ['version', 'serial', 'lineage', 'outputs', 'resources']))
|
|
259
|
+
check('unknown credential denied', request('writer-a', 'GET', '/state', password='invalid-fixture-password') == 401)
|
|
260
|
+
control.pause = True
|
|
261
|
+
active = subprocess.Popen([str(terraform), 'plan', '-input=false', '-no-color', '-lock-timeout=0s'], cwd=private / 'writer-a', env=environment('writer-a'), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
262
|
+
check('real native plan holds lock while reading', control.read_held.wait(12) and active.poll() is None and store.lock() is not None)
|
|
263
|
+
current = store.lock()[1]
|
|
264
|
+
competing = native('writer-b', ['plan', '-input=false', '-no-color', '-lock-timeout=0s'], expected_exit=1)
|
|
265
|
+
check('second native writer excluded', competing.returncode == 1 and 'Error acquiring the state lock' in competing.stderr + competing.stdout)
|
|
266
|
+
check('foreign actor cannot unlock owner', request('writer-b', 'DELETE', '/lock', {'ID': current['ID']}) == 409 and store.lock()[1]['ID'] == current['ID'])
|
|
267
|
+
check('incorrect lock ID cannot unlock owner', request('writer-a', 'DELETE', '/lock', {'ID': 'wrong-fixture-lock'}) == 409 and store.lock()[1]['ID'] == current['ID'])
|
|
268
|
+
check('foreign writer cannot replace state', request('writer-b', 'POST', '/state?ID=' + current['ID'], {}) == 409 and store.read() == initial)
|
|
269
|
+
try:
|
|
270
|
+
store.restore_fixture_version(1)
|
|
271
|
+
refused = False
|
|
272
|
+
except ValueError:
|
|
273
|
+
refused = True
|
|
274
|
+
check('recovery refuses an active writer', refused)
|
|
275
|
+
control.pause = False
|
|
276
|
+
control.release.set()
|
|
277
|
+
output, _ = active.communicate(timeout=15)
|
|
278
|
+
(private / 'writer-a-plan.log').write_text(output)
|
|
279
|
+
commands.append({'actor': 'writer-a', 'arguments': ['plan', '-input=false', '-no-color', '-lock-timeout=0s'], 'exitCode': active.returncode, 'expectedExitCode': 0})
|
|
280
|
+
check('native plan succeeds and releases owned lock', active.returncode == 0 and store.lock() is None)
|
|
281
|
+
check('plan-only clients never write state', not any(e['method'] == 'POST' and e['path'] == '/state' and e['actor'] == 'writer-a' for e in control.events) and store.read() == initial)
|
|
282
|
+
changed = json.loads(initial)
|
|
283
|
+
changed['serial'] = 2
|
|
284
|
+
changed['outputs']['fixture']['value'] = 'synthetic-new-version'
|
|
285
|
+
changed_id = store.add_fixture_version(encoded(changed))
|
|
286
|
+
reopened = FixtureStore(private / 'fixture.sqlite', b'not-a-new-seed')
|
|
287
|
+
check('local fixture versions survive reopen', json.loads(reopened.read())['serial'] == 2)
|
|
288
|
+
reopened.restore_fixture_version(1)
|
|
289
|
+
check('verified fixture recovery restores exact bytes', reopened.read() == initial)
|
|
290
|
+
restored = native('writer-b', ['state', 'pull'])
|
|
291
|
+
check('native observes restored lineage and payload', restored.returncode == 0 and json.loads(restored.stdout) == json.loads(pulled.stdout))
|
|
292
|
+
with reopened.connection() as db:
|
|
293
|
+
db.execute('UPDATE versions SET payload=? WHERE id=?', (b'corrupted-synthetic-backup', changed_id))
|
|
294
|
+
try:
|
|
295
|
+
reopened.restore_fixture_version(changed_id)
|
|
296
|
+
refused = False
|
|
297
|
+
except ValueError:
|
|
298
|
+
refused = True
|
|
299
|
+
check('corrupt fixture backup cannot replace good state', refused and reopened.read() == initial)
|
|
300
|
+
abandoned = {'ID': str(uuid.uuid4()), 'Operation': 'synthetic-abandoned-writer'}
|
|
301
|
+
reopened.acquire('writer-a', abandoned)
|
|
302
|
+
restarted = FixtureStore(private / 'fixture.sqlite', b'not-a-new-seed')
|
|
303
|
+
check('uncertain abandoned lock persists across reopen', restarted.lock()[1]['ID'] == abandoned['ID'])
|
|
304
|
+
check('abandoned lock still needs matching owner and ID', restarted.unlock('writer-b', abandoned)[0] == 409 and restarted.unlock('writer-a', abandoned)[0] == 200)
|
|
305
|
+
finally:
|
|
306
|
+
control.pause = False
|
|
307
|
+
control.release.set()
|
|
308
|
+
if active is not None and active.poll() is None:
|
|
309
|
+
active.terminate()
|
|
310
|
+
try:
|
|
311
|
+
active.communicate(timeout=5)
|
|
312
|
+
except subprocess.TimeoutExpired:
|
|
313
|
+
active.kill()
|
|
314
|
+
active.communicate(timeout=5)
|
|
315
|
+
server.shutdown()
|
|
316
|
+
server.server_close()
|
|
317
|
+
thread.join(timeout=5)
|
|
318
|
+
report = {'passed': all(c['passed'] for c in checks), 'scope': 'isolated-loopback-http-state-protocol',
|
|
319
|
+
'sourceSha256': digest(Path(__file__).read_bytes()), 'terraformVersion': version['terraform_version'],
|
|
320
|
+
'terraformSha256': digest(terraform.read_bytes()), 'checks': checks, 'commands': commands,
|
|
321
|
+
'initialSyntheticStateSha256': digest(initial), 'selectedBackendVerified': False,
|
|
322
|
+
'migrationAuthorized': False, 'boundary': 'Actual Terraform init/plan/state pull against a synthetic loopback HTTP fixture and local SQLite version/lock store. No apply, project backend, TLS, cloud authentication, S3/Azure/GCS behavior, migration or application-system qualification.'}
|
|
323
|
+
serialized = json.dumps(report, indent=2) + '\n'
|
|
324
|
+
if any(secret in serialized for secret in credentials.values()):
|
|
325
|
+
raise ValueError('Credential appeared in public fixture evidence')
|
|
326
|
+
(folder / 'report.json').write_text(serialized)
|
|
327
|
+
return report
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
if __name__ == '__main__':
|
|
331
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
332
|
+
parser.add_argument('--terraform', type=Path, default=shutil.which('terraform'))
|
|
333
|
+
parser.add_argument('--work-dir', type=Path, required=True)
|
|
334
|
+
args = parser.parse_args()
|
|
335
|
+
if not args.terraform:
|
|
336
|
+
parser.error('Install a local Terraform executable or provide --terraform.')
|
|
337
|
+
result = run_probe(Path(args.terraform).resolve(strict=True), args.work_dir.resolve())
|
|
338
|
+
print(json.dumps({'passed': result['passed'], 'checks': len(result['checks']), 'report': str(args.work_dir / 'report.json'), 'selectedBackendVerified': False}))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"\"\"\"Provider-neutral flat output boundary. Never executes model-supplied actions.\"\"\"\nimport json\nimport math\n\nclass InvalidModelOutput(ValueError):\n pass\n\ndef _object(pairs):\n result = {}\n for key, value in pairs:\n if key in result:\n raise InvalidModelOutput('Duplicate field')\n result[key] = value\n return result\n\ndef decode_output(text, fields):\n if not isinstance(text, str):\n raise InvalidModelOutput('Expected JSON text')\n try:\n if len(text.encode('utf-8')) > 131072:\n raise InvalidModelOutput('Oversized output')\n value = json.loads(text, object_pairs_hook=_object,\n parse_constant=lambda _: (_ for _ in ()).throw(InvalidModelOutput('Non-finite number')))\n except (ValueError, UnicodeError, RecursionError) as error:\n raise InvalidModelOutput('Invalid JSON output') from None\n if not isinstance(value, dict) or set(value) != {f['name'] for f in fields}:\n raise InvalidModelOutput('Output fields do not match contract')\n for field in fields:\n item = value[field['name']]\n kind = field['type']\n if kind == 'string':\n if not isinstance(item, str) or len(item) > field.get('maxLength', 4096):\n raise InvalidModelOutput('Invalid string output')\n try:\n item.encode('utf-8')\n except UnicodeError:\n raise InvalidModelOutput('Invalid Unicode output') from None\n if 'values' in field and item not in field['values']:\n raise InvalidModelOutput('Value outside enumeration')\n elif kind == 'boolean':\n if type(item) is not bool:\n raise InvalidModelOutput('Invalid boolean output')\n elif kind == 'number':\n if type(item) not in (int, float) or abs(item) > 1e308 or not math.isfinite(item):\n raise InvalidModelOutput('Invalid numeric output')\n else:\n raise InvalidModelOutput('Unsupported output contract')\n return value\n"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const byId=id=>document.getElementById(id);
|
|
3
|
+
let config,accessToken=null,expiresAt=0,pending=null,sessionEpoch=0;
|
|
4
|
+
const status=text=>{byId('status').textContent=text;};
|
|
5
|
+
const signedIn=()=>!!accessToken&&Date.now()<expiresAt;
|
|
6
|
+
function ready(){
|
|
7
|
+
const enabled=signedIn();byId('question').disabled=!enabled;byId('submit').disabled=!enabled||!!pending;
|
|
8
|
+
byId('sign-in').hidden=enabled;byId('sign-out').hidden=!enabled;
|
|
9
|
+
}
|
|
10
|
+
function clearPrivateView(){byId('question').value='';byId('answer').replaceChildren();byId('citations').replaceChildren();}
|
|
11
|
+
function endSession(){sessionEpoch++;accessToken=null;expiresAt=0;if(pending)pending.abort();pending=null;sessionStorage.removeItem('atmem-oauth');clearPrivateView();ready();}
|
|
12
|
+
const random=()=>Array.from(crypto.getRandomValues(new Uint8Array(32)),b=>b.toString(16).padStart(2,'0')).join('');
|
|
13
|
+
function endpoint(value,fallback){const result=new URL(value||fallback,config.authOrigin);if(result.protocol!=='https:')throw Error('Invalid sign-in endpoint');return result;}
|
|
14
|
+
async function signIn(){
|
|
15
|
+
const verifier=random(),state=random();sessionStorage.setItem('atmem-oauth',JSON.stringify({verifier,state,created:Date.now()}));
|
|
16
|
+
const digest=new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(verifier)));
|
|
17
|
+
const challenge=btoa(String.fromCharCode(...digest)).replaceAll('+','-').replaceAll('/','_').replaceAll('=','');
|
|
18
|
+
const url=endpoint(config.authorizationEndpoint,'/oauth2/authorize');
|
|
19
|
+
url.search=new URLSearchParams({client_id:config.clientId,response_type:'code',scope:config.scopes||'openid atmem/ask',redirect_uri:location.origin+'/',state,code_challenge:challenge,code_challenge_method:'S256'});location.assign(url);
|
|
20
|
+
}
|
|
21
|
+
async function initialize(){
|
|
22
|
+
const response=await fetch('/config.json',{cache:'no-store'});if(!response.ok)throw Error('Configuration unavailable');config=await response.json();
|
|
23
|
+
if(new URL(config.authOrigin).protocol!=='https:'||typeof config.clientId!=='string'||!Number.isInteger(config.maxInputBytes)||config.maxInputBytes<1||config.maxInputBytes>1000000)throw Error('Invalid configuration');
|
|
24
|
+
config.fields=JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(config.fieldsBase64),c=>c.charCodeAt(0))));
|
|
25
|
+
if(!Array.isArray(config.fields)||!config.fields.length||config.fields.length>24||config.fields.some(f=>!f||typeof f.name!=='string'||typeof f.description!=='string'||!['string','boolean','number'].includes(f.type)))throw Error('Invalid result configuration');
|
|
26
|
+
byId('question').maxLength=config.maxInputBytes;
|
|
27
|
+
const query=new URLSearchParams(location.search),code=query.get('code');
|
|
28
|
+
if(query.has('error')){history.replaceState(null,'','/');sessionStorage.removeItem('atmem-oauth');throw Error('Sign-in was not completed');}
|
|
29
|
+
if(code){
|
|
30
|
+
const saved=JSON.parse(sessionStorage.getItem('atmem-oauth')||'null');sessionStorage.removeItem('atmem-oauth');history.replaceState(null,'','/');
|
|
31
|
+
if(!saved||saved.state!==query.get('state')||!Number.isFinite(saved.created)||Date.now()-saved.created>600000||saved.created>Date.now()+5000)throw Error('Sign-in state expired or mismatched');
|
|
32
|
+
const tokenResponse=await fetch(endpoint(config.tokenEndpoint,'/oauth2/token'),{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({grant_type:'authorization_code',client_id:config.clientId,code,redirect_uri:location.origin+'/',code_verifier:saved.verifier})});
|
|
33
|
+
if(!tokenResponse.ok)throw Error('Sign-in could not be completed');const tokens=await tokenResponse.json();
|
|
34
|
+
if(typeof tokens.access_token!=='string'||!tokens.access_token||!Number.isFinite(tokens.expires_in)||tokens.expires_in<=0)throw Error('Invalid sign-in response');
|
|
35
|
+
accessToken=tokens.access_token;expiresAt=Date.now()+tokens.expires_in*1000;sessionEpoch++;ready();status('Signed in. Submit input for a result you can review.');
|
|
36
|
+
}else{ready();status('Sign in to use this workspace.');}
|
|
37
|
+
}
|
|
38
|
+
function renderResult(result){
|
|
39
|
+
if(!result.result||typeof result.result!=='object'||Array.isArray(result.result)||result.requiresHumanReview!==true)throw Error('The result could not be verified.');
|
|
40
|
+
const valid=Object.keys(result.result).length===config.fields.length&&config.fields.every(field=>{
|
|
41
|
+
const value=result.result[field.name];return typeof value===field.type&&(field.type!=='number'||Number.isFinite(value))&&(field.type!=='string'||Array.from(value).length<=(field.maxLength||4096))&&(!field.values||field.values.includes(value));
|
|
42
|
+
});
|
|
43
|
+
if(!valid)throw Error('The result could not be verified.');
|
|
44
|
+
const fragment=document.createDocumentFragment();
|
|
45
|
+
for(const field of config.fields){const value=result.result[field.name],label=document.createElement('dt'),item=document.createElement('dd');label.textContent=field.description;item.textContent=typeof value==='boolean'?(value?'Yes':'No'):String(value);fragment.append(label,item);}
|
|
46
|
+
byId('answer').replaceChildren(fragment);status('Result prepared. Review it before taking action.');
|
|
47
|
+
}
|
|
48
|
+
byId('sign-in').addEventListener('click',()=>signIn().catch(()=>status('Sign-in could not be started. Please retry.')));
|
|
49
|
+
byId('sign-out').addEventListener('click',()=>{
|
|
50
|
+
endSession();
|
|
51
|
+
if(config.localLogout===true){status('Workspace cleared. Your identity-provider session may still be active. Close this browser on a shared device.');return;}
|
|
52
|
+
const url=endpoint(config.logoutEndpoint,'/logout');url.search=new URLSearchParams({client_id:config.clientId,[config.logoutParameter||'logout_uri']:location.origin+'/'});location.assign(url);
|
|
53
|
+
});
|
|
54
|
+
byId('ask').addEventListener('submit',async event=>{
|
|
55
|
+
event.preventDefault();if(pending)return;
|
|
56
|
+
if(!signedIn()){endSession();status('Your session expired. Sign in again.');return;}
|
|
57
|
+
const text=byId('question').value.trim();if(!text||new TextEncoder().encode(text).length>config.maxInputBytes){status('Use input of at most '+config.maxInputBytes.toLocaleString()+' UTF-8 bytes.');return;}
|
|
58
|
+
const epoch=sessionEpoch,token=accessToken,controller=new AbortController();pending=controller;ready();byId('answer').replaceChildren();status('Preparing a result…');
|
|
59
|
+
const timer=setTimeout(()=>controller.abort(),config.requestDeadlineMs||28000);
|
|
60
|
+
try{
|
|
61
|
+
const response=await fetch('/api/ask',{method:'POST',headers:{'Content-Type':'application/json',Authorization:'Bearer '+token},body:JSON.stringify({text}),signal:controller.signal,cache:'no-store'});
|
|
62
|
+
if(epoch!==sessionEpoch||accessToken!==token)return;
|
|
63
|
+
if(response.status===401){endSession();status('Your session expired. Sign in again.');return;}
|
|
64
|
+
if(response.status===403){endSession();status('Access is unavailable or permissions changed. Sign in again or contact the app owner.');return;}
|
|
65
|
+
const result=await response.json();if(epoch!==sessionEpoch||accessToken!==token)return;
|
|
66
|
+
if(!response.ok){if(result.error==='data_retention_unverified')throw Error('This workspace needs its data-handling setup checked by the app owner. Your input was not sent to the AI.');throw Error('The AI result is unavailable or could not be verified. Please try again later.');}
|
|
67
|
+
renderResult(result);
|
|
68
|
+
}catch(error){if(epoch===sessionEpoch&&accessToken===token)status(error.name==='AbortError'?'The request timed out. Please try again later.':error.message);}
|
|
69
|
+
finally{clearTimeout(timer);if(pending===controller)pending=null;ready();}
|
|
70
|
+
});
|
|
71
|
+
initialize().catch(()=>{endSession();status('Sign-in or configuration could not be verified. Please retry sign-in.');});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"'use strict';\nconst byId=id=>document.getElementById(id);\nlet config,accessToken=null,expiresAt=0,pending=null,sessionEpoch=0;\nconst status=text=>{byId('status').textContent=text;};\nconst signedIn=()=>!!accessToken&&Date.now()<expiresAt;\nfunction ready(){\n const enabled=signedIn();byId('question').disabled=!enabled;byId('submit').disabled=!enabled||!!pending;\n byId('sign-in').hidden=enabled;byId('sign-out').hidden=!enabled;\n}\nfunction clearPrivateView(){byId('question').value='';byId('answer').replaceChildren();byId('citations').replaceChildren();}\nfunction endSession(){sessionEpoch++;accessToken=null;expiresAt=0;if(pending)pending.abort();pending=null;sessionStorage.removeItem('atmem-oauth');clearPrivateView();ready();}\nconst random=()=>Array.from(crypto.getRandomValues(new Uint8Array(32)),b=>b.toString(16).padStart(2,'0')).join('');\nfunction endpoint(value,fallback){const result=new URL(value||fallback,config.authOrigin);if(result.protocol!=='https:')throw Error('Invalid sign-in endpoint');return result;}\nasync function signIn(){\n const verifier=random(),state=random();sessionStorage.setItem('atmem-oauth',JSON.stringify({verifier,state,created:Date.now()}));\n const digest=new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(verifier)));\n const challenge=btoa(String.fromCharCode(...digest)).replaceAll('+','-').replaceAll('/','_').replaceAll('=','');\n const url=endpoint(config.authorizationEndpoint,'/oauth2/authorize');\n url.search=new URLSearchParams({client_id:config.clientId,response_type:'code',scope:config.scopes||'openid atmem/ask',redirect_uri:location.origin+'/',state,code_challenge:challenge,code_challenge_method:'S256'});location.assign(url);\n}\nasync function initialize(){\n const response=await fetch('/config.json',{cache:'no-store'});if(!response.ok)throw Error('Configuration unavailable');config=await response.json();\n if(new URL(config.authOrigin).protocol!=='https:'||typeof config.clientId!=='string'||!Number.isInteger(config.maxInputBytes)||config.maxInputBytes<1||config.maxInputBytes>1000000)throw Error('Invalid configuration');\n config.fields=JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(config.fieldsBase64),c=>c.charCodeAt(0))));\n if(!Array.isArray(config.fields)||!config.fields.length||config.fields.length>24||config.fields.some(f=>!f||typeof f.name!=='string'||typeof f.description!=='string'||!['string','boolean','number'].includes(f.type)))throw Error('Invalid result configuration');\n byId('question').maxLength=config.maxInputBytes;\n const query=new URLSearchParams(location.search),code=query.get('code');\n if(query.has('error')){history.replaceState(null,'','/');sessionStorage.removeItem('atmem-oauth');throw Error('Sign-in was not completed');}\n if(code){\n const saved=JSON.parse(sessionStorage.getItem('atmem-oauth')||'null');sessionStorage.removeItem('atmem-oauth');history.replaceState(null,'','/');\n if(!saved||saved.state!==query.get('state')||!Number.isFinite(saved.created)||Date.now()-saved.created>600000||saved.created>Date.now()+5000)throw Error('Sign-in state expired or mismatched');\n const tokenResponse=await fetch(endpoint(config.tokenEndpoint,'/oauth2/token'),{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({grant_type:'authorization_code',client_id:config.clientId,code,redirect_uri:location.origin+'/',code_verifier:saved.verifier})});\n if(!tokenResponse.ok)throw Error('Sign-in could not be completed');const tokens=await tokenResponse.json();\n if(typeof tokens.access_token!=='string'||!tokens.access_token||!Number.isFinite(tokens.expires_in)||tokens.expires_in<=0)throw Error('Invalid sign-in response');\n accessToken=tokens.access_token;expiresAt=Date.now()+tokens.expires_in*1000;sessionEpoch++;ready();status('Signed in. Submit input for a result you can review.');\n }else{ready();status('Sign in to use this workspace.');}\n}\nfunction renderResult(result){\n if(!result.result||typeof result.result!=='object'||Array.isArray(result.result)||result.requiresHumanReview!==true)throw Error('The result could not be verified.');\n const valid=Object.keys(result.result).length===config.fields.length&&config.fields.every(field=>{\n const value=result.result[field.name];return typeof value===field.type&&(field.type!=='number'||Number.isFinite(value))&&(field.type!=='string'||Array.from(value).length<=(field.maxLength||4096))&&(!field.values||field.values.includes(value));\n });\n if(!valid)throw Error('The result could not be verified.');\n const fragment=document.createDocumentFragment();\n for(const field of config.fields){const value=result.result[field.name],label=document.createElement('dt'),item=document.createElement('dd');label.textContent=field.description;item.textContent=typeof value==='boolean'?(value?'Yes':'No'):String(value);fragment.append(label,item);}\n byId('answer').replaceChildren(fragment);status('Result prepared. Review it before taking action.');\n}\nbyId('sign-in').addEventListener('click',()=>signIn().catch(()=>status('Sign-in could not be started. Please retry.')));\nbyId('sign-out').addEventListener('click',()=>{\n endSession();\n if(config.localLogout===true){status('Workspace cleared. Your identity-provider session may still be active. Close this browser on a shared device.');return;}\n const url=endpoint(config.logoutEndpoint,'/logout');url.search=new URLSearchParams({client_id:config.clientId,[config.logoutParameter||'logout_uri']:location.origin+'/'});location.assign(url);\n});\nbyId('ask').addEventListener('submit',async event=>{\n event.preventDefault();if(pending)return;\n if(!signedIn()){endSession();status('Your session expired. Sign in again.');return;}\n const text=byId('question').value.trim();if(!text||new TextEncoder().encode(text).length>config.maxInputBytes){status('Use input of at most '+config.maxInputBytes.toLocaleString()+' UTF-8 bytes.');return;}\n const epoch=sessionEpoch,token=accessToken,controller=new AbortController();pending=controller;ready();byId('answer').replaceChildren();status('Preparing a result…');\n const timer=setTimeout(()=>controller.abort(),config.requestDeadlineMs||28000);\n try{\n const response=await fetch('/api/ask',{method:'POST',headers:{'Content-Type':'application/json',Authorization:'Bearer '+token},body:JSON.stringify({text}),signal:controller.signal,cache:'no-store'});\n if(epoch!==sessionEpoch||accessToken!==token)return;\n if(response.status===401){endSession();status('Your session expired. Sign in again.');return;}\n if(response.status===403){endSession();status('Access is unavailable or permissions changed. Sign in again or contact the app owner.');return;}\n const result=await response.json();if(epoch!==sessionEpoch||accessToken!==token)return;\n if(!response.ok){if(result.error==='data_retention_unverified')throw Error('This workspace needs its data-handling setup checked by the app owner. Your input was not sent to the AI.');throw Error('The AI result is unavailable or could not be verified. Please try again later.');}\n renderResult(result);\n }catch(error){if(epoch===sessionEpoch&&accessToken===token)status(error.name==='AbortError'?'The request timed out. Please try again later.':error.message);}\n finally{clearTimeout(timer);if(pending===controller)pending=null;ready();}\n});\ninitialize().catch(()=>{endSession();status('Sign-in or configuration could not be verified. Please retry sign-in.');});\n"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Provider-neutral HTTP boundary; identity and model transports are explicit adapters."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from fastapi import FastAPI, Request
|
|
7
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
8
|
+
from starlette.concurrency import run_in_threadpool
|
|
9
|
+
from structured_output import _object
|
|
10
|
+
|
|
11
|
+
class PolicyUnverified(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
class AccessDenied(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def create_app(config, verify, infer, public_config, static_root=None, token_header=None, protect_assets=False, require_origin=False, origin_validator=None):
|
|
18
|
+
root = Path(static_root or Path(__file__).parent / "web")
|
|
19
|
+
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
|
|
20
|
+
# Small internal apps admit a burst of five model requests per process.
|
|
21
|
+
# Capacity is bounded; excess callers receive a retryable 429, never a hidden
|
|
22
|
+
# unbounded queue. Destination quota/latency still needs real qualification.
|
|
23
|
+
slots = threading.BoundedSemaphore(5)
|
|
24
|
+
|
|
25
|
+
@app.middleware("http")
|
|
26
|
+
async def headers(request, next_handler):
|
|
27
|
+
denied = False
|
|
28
|
+
if protect_assets and request.url.path != "/healthz":
|
|
29
|
+
try:
|
|
30
|
+
await run_in_threadpool(verify, request.headers.get(token_header, ""))
|
|
31
|
+
except Exception:
|
|
32
|
+
denied = True
|
|
33
|
+
response = JSONResponse({"error": "access_denied"}, status_code=401) if denied else await next_handler(request)
|
|
34
|
+
response.headers.update({"Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer",
|
|
35
|
+
"Content-Security-Policy": config.get("csp", "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"), "Strict-Transport-Security": "max-age=31536000"})
|
|
36
|
+
return response
|
|
37
|
+
|
|
38
|
+
@app.get("/healthz")
|
|
39
|
+
def health():
|
|
40
|
+
# Startup liveness, not proof that identity/model/account policy is ready.
|
|
41
|
+
return {"status": "running"}
|
|
42
|
+
|
|
43
|
+
@app.get("/config.json")
|
|
44
|
+
def get_public_config():
|
|
45
|
+
return public_config()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.get("/")
|
|
49
|
+
def index():
|
|
50
|
+
return FileResponse(root / "index.html")
|
|
51
|
+
|
|
52
|
+
@app.get("/app.js")
|
|
53
|
+
def script():
|
|
54
|
+
return FileResponse(root / "app.js", media_type="text/javascript")
|
|
55
|
+
|
|
56
|
+
@app.get("/app.css")
|
|
57
|
+
def style():
|
|
58
|
+
return FileResponse(root / "app.css", media_type="text/css")
|
|
59
|
+
|
|
60
|
+
@app.post("/api/ask")
|
|
61
|
+
async def ask(request: Request):
|
|
62
|
+
origin = request.headers.get("origin")
|
|
63
|
+
origin_ok=origin_validator(origin,request.headers.get('host','')) if origin_validator else not ((require_origin and not origin) or (origin and origin != config["PUBLIC_ORIGIN"]))
|
|
64
|
+
if not origin_ok:
|
|
65
|
+
return JSONResponse({"error": "access_denied"}, status_code=403)
|
|
66
|
+
auth = request.headers.get("authorization", "")
|
|
67
|
+
token = request.headers.get(token_header, "") if token_header else auth[7:] if auth.startswith("Bearer ") else ""
|
|
68
|
+
try:
|
|
69
|
+
await run_in_threadpool(verify, token)
|
|
70
|
+
except Exception:
|
|
71
|
+
return JSONResponse({"error": "access_denied"}, status_code=401)
|
|
72
|
+
if request.headers.get("content-type", "").split(";")[0].lower() != "application/json":
|
|
73
|
+
return JSONResponse({"error": "invalid_request"}, status_code=400)
|
|
74
|
+
if not slots.acquire(blocking=False):
|
|
75
|
+
return JSONResponse({"error": "busy"}, status_code=429)
|
|
76
|
+
try:
|
|
77
|
+
raw = bytearray()
|
|
78
|
+
async with asyncio.timeout(5):
|
|
79
|
+
async for chunk in request.stream():
|
|
80
|
+
raw.extend(chunk)
|
|
81
|
+
if len(raw) > config["max_input"] * 6 + 100:
|
|
82
|
+
return JSONResponse({"error": "input_too_large"}, status_code=413)
|
|
83
|
+
body = json.loads(raw.decode("utf-8"), object_pairs_hook=_object)
|
|
84
|
+
text = body.get("text") if isinstance(body, dict) and set(body) == {"text"} else None
|
|
85
|
+
if not isinstance(text, str) or not text.strip() or len(text.encode("utf-8")) > config["max_input"]:
|
|
86
|
+
return JSONResponse({"error": "invalid_request"}, status_code=400)
|
|
87
|
+
try:
|
|
88
|
+
result = await run_in_threadpool(infer, text)
|
|
89
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
90
|
+
return JSONResponse({"error": "invalid_model_output"}, status_code=503)
|
|
91
|
+
# Never return an inference result after expiry, or if injected verification fails.
|
|
92
|
+
await run_in_threadpool(verify, token)
|
|
93
|
+
return {"result": result, "requiresHumanReview": True}
|
|
94
|
+
except PolicyUnverified:
|
|
95
|
+
return JSONResponse({"error": "data_retention_unverified"}, status_code=503)
|
|
96
|
+
except AccessDenied:
|
|
97
|
+
return JSONResponse({"error": "access_denied"}, status_code=403)
|
|
98
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
99
|
+
return JSONResponse({"error": "invalid_request_or_output"}, status_code=400)
|
|
100
|
+
except Exception:
|
|
101
|
+
return JSONResponse({"error": "temporarily_unavailable"}, status_code=503)
|
|
102
|
+
finally:
|
|
103
|
+
slots.release()
|
|
104
|
+
return app
|
|
105
|
+
|