@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,599 @@
|
|
|
1
|
+
"""Root-owned Batch prepare/release/reaper; fixed local Docker Engine only.
|
|
2
|
+
|
|
3
|
+
Standalone standard-library helper. No document bytes pass through this process.
|
|
4
|
+
The trusted transfer artifact alone reads Blob/input/result bytes. This helper is
|
|
5
|
+
not a cloud provisioning contract or evidence that a Batch node is configured.
|
|
6
|
+
"""
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
import base64
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from email.utils import format_datetime
|
|
11
|
+
import fcntl
|
|
12
|
+
import hashlib
|
|
13
|
+
import http.client
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import re
|
|
18
|
+
import socket
|
|
19
|
+
import stat
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from urllib.parse import quote, urlencode
|
|
23
|
+
import uuid
|
|
24
|
+
|
|
25
|
+
API = '/v1.43'
|
|
26
|
+
MAX_JSON = 1048576
|
|
27
|
+
HASH = r'[0-9a-f]{64}'
|
|
28
|
+
UUID = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
|
29
|
+
STATE_FIELDS = {'format', 'requestId', 'nonce', 'owner', 'policy', 'helperSha256',
|
|
30
|
+
'parserImage', 'transferImage', 'transfer', 'createdAt', 'expiresAt', 'phase', 'published', 'receiptPublished', 'nodeId'}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def unique(pairs):
|
|
34
|
+
value = {}
|
|
35
|
+
for key, item in pairs:
|
|
36
|
+
if key in value: raise ValueError('Ambiguous owned metadata')
|
|
37
|
+
value[key] = item
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def encoded(value):
|
|
42
|
+
return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def digest(value):
|
|
46
|
+
return hashlib.sha256(encoded(value)).hexdigest()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def match(value, pattern):
|
|
50
|
+
return isinstance(value, str) and re.fullmatch(pattern, value) is not None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def node_identity(value):
|
|
54
|
+
if not match(value, r'[A-Za-z0-9_-]{1,128}'): raise ValueError('Use the exact native node identity')
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def image(value):
|
|
59
|
+
if not match(value, r'[a-z0-9]{5,50}\.azurecr\.io/[a-z0-9]+(?:[._/-][a-z0-9]+)*@sha256:' + HASH):
|
|
60
|
+
raise ValueError('Use an immutable owned ACR artifact')
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def policy(value):
|
|
65
|
+
fields = {'format', 'subscriptionId', 'region', 'resourceGroup', 'batchAccount', 'poolId',
|
|
66
|
+
'storageAccount', 'container', 'workerClientId', 'transferClientId',
|
|
67
|
+
'parserImage', 'transferImage', 'hostHelperSha256'}
|
|
68
|
+
if not isinstance(value, dict) or set(value) != fields or value['format'] != 'enterprise-azure-document-job-v1':
|
|
69
|
+
raise ValueError('Use the exact root-owned host policy')
|
|
70
|
+
patterns = {'subscriptionId': UUID, 'region': r'[a-z][a-z0-9]{1,30}',
|
|
71
|
+
'resourceGroup': r'[A-Za-z0-9_()-](?:[A-Za-z0-9_().-]{0,88}[A-Za-z0-9_()-])?',
|
|
72
|
+
'batchAccount': r'[a-z0-9]{3,24}', 'poolId': r'document-[a-z0-9-]{1,54}',
|
|
73
|
+
'storageAccount': r'[a-z0-9]{3,24}', 'container': r'[a-z0-9]+(?:-[a-z0-9]+)*',
|
|
74
|
+
'workerClientId': UUID, 'transferClientId': UUID, 'hostHelperSha256': HASH}
|
|
75
|
+
if any(not match(value[key], pattern) for key, pattern in patterns.items()) or not 3 <= len(value['container']) <= 63:
|
|
76
|
+
raise ValueError('Invalid owned host destination')
|
|
77
|
+
if value['workerClientId'] == value['transferClientId']:
|
|
78
|
+
raise ValueError('Worker identity must not be on the transfer node')
|
|
79
|
+
for key in ['parserImage', 'transferImage']: image(value[key])
|
|
80
|
+
if (value['parserImage'] == value['transferImage'] or
|
|
81
|
+
value['parserImage'].split('/')[0] != value['transferImage'].split('/')[0]):
|
|
82
|
+
raise ValueError('Use distinct artifacts from the same owned registry')
|
|
83
|
+
return dict(value)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def owner(value):
|
|
87
|
+
checked = policy(value)
|
|
88
|
+
keys = ['subscriptionId', 'region', 'resourceGroup', 'batchAccount', 'poolId',
|
|
89
|
+
'storageAccount', 'container', 'transferClientId']
|
|
90
|
+
return digest({**{key: checked[key] for key in keys}, 'registry': checked['parserImage'].split('/')[0]})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def volumes(identifier):
|
|
94
|
+
if not match(identifier, HASH): raise ValueError('Invalid owned job identity')
|
|
95
|
+
return ['atmem-document-' + identifier + '-' + side for side in ['input', 'output']]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def transfer(value, config):
|
|
99
|
+
keys = {'format', 'subscriptionId', 'region', 'resourceGroup', 'storageAccount',
|
|
100
|
+
'container', 'transferClientId', 'request', 'inputETag'}
|
|
101
|
+
if not isinstance(value, dict) or set(value) != keys or value['format'] != 'enterprise-azure-document-transfer-v1':
|
|
102
|
+
raise ValueError('Use exact transfer metadata')
|
|
103
|
+
for key in ['subscriptionId', 'region', 'resourceGroup', 'storageAccount', 'container', 'transferClientId']:
|
|
104
|
+
if value[key] != config[key]: raise ValueError('Transfer destination changed')
|
|
105
|
+
request = value['request']
|
|
106
|
+
if (not isinstance(request, dict) or set(request) != {'format', 'requestId', 'sha256', 'mediaType'} or
|
|
107
|
+
request['format'] != 'enterprise-document-job-v1' or not match(request['requestId'], HASH) or
|
|
108
|
+
not match(request['sha256'], HASH) or not match(request['mediaType'], r'[a-z0-9.+-]+/[a-z0-9.+-]+') or
|
|
109
|
+
len(request['mediaType']) > 200 or not match(value['inputETag'], r'"0x[0-9A-Fa-f]{1,32}"')):
|
|
110
|
+
raise ValueError('Invalid byte-bound transfer metadata')
|
|
111
|
+
# Full supported-MIME validation remains in the independently versioned
|
|
112
|
+
# transfer artifact; this host never opens or parses the file.
|
|
113
|
+
if len(encoded(value)) > 4096: raise ValueError('Transfer metadata exceeds host limit')
|
|
114
|
+
return json.loads(encoded(value), object_pairs_hook=unique)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def protected_directory(path, *, uid=0):
|
|
118
|
+
"""Walk every absolute ancestor without following symlinks, including /var."""
|
|
119
|
+
path = Path(path)
|
|
120
|
+
if not path.is_absolute() or '..' in path.parts: raise ValueError('Use an absolute protected directory')
|
|
121
|
+
fd = os.open('/', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
122
|
+
try:
|
|
123
|
+
for part in path.parts[1:]:
|
|
124
|
+
child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd)
|
|
125
|
+
os.close(fd); fd = child
|
|
126
|
+
info = os.fstat(fd)
|
|
127
|
+
if info.st_uid != uid or stat.S_IMODE(info.st_mode) & 0o022:
|
|
128
|
+
raise ValueError('Host directory is not protected by its owner')
|
|
129
|
+
return fd
|
|
130
|
+
except BaseException:
|
|
131
|
+
os.close(fd)
|
|
132
|
+
raise
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def owned_file(directory, name, *, uid=0, limit=16384):
|
|
136
|
+
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory)
|
|
137
|
+
with os.fdopen(fd, 'rb') as stream:
|
|
138
|
+
info = os.fstat(stream.fileno())
|
|
139
|
+
if (not stat.S_ISREG(info.st_mode) or info.st_uid != uid or info.st_nlink != 1 or
|
|
140
|
+
stat.S_IMODE(info.st_mode) & 0o022 or not 0 < info.st_size <= limit):
|
|
141
|
+
raise ValueError('Invalid protected host metadata file')
|
|
142
|
+
raw = stream.read(limit + 1)
|
|
143
|
+
if len(raw) != info.st_size: raise ValueError('Host metadata changed')
|
|
144
|
+
return raw
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Store:
|
|
148
|
+
def __init__(self, directory, *, uid=0):
|
|
149
|
+
info = os.fstat(directory)
|
|
150
|
+
if not stat.S_ISDIR(info.st_mode) or info.st_uid != uid or stat.S_IMODE(info.st_mode) != 0o700:
|
|
151
|
+
raise ValueError('Use the private root-owned state directory')
|
|
152
|
+
self.fd, self.uid = os.dup(directory), uid
|
|
153
|
+
|
|
154
|
+
def close(self): os.close(self.fd)
|
|
155
|
+
|
|
156
|
+
@contextmanager
|
|
157
|
+
def lock(self, identifier):
|
|
158
|
+
volumes(identifier)
|
|
159
|
+
fd = os.open(identifier + '.lock', os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600, dir_fd=self.fd)
|
|
160
|
+
try:
|
|
161
|
+
info = os.fstat(fd)
|
|
162
|
+
if not stat.S_ISREG(info.st_mode) or info.st_uid != self.uid or info.st_nlink != 1 or stat.S_IMODE(info.st_mode) != 0o600:
|
|
163
|
+
raise ValueError('Invalid owned state lock')
|
|
164
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
165
|
+
yield
|
|
166
|
+
finally: os.close(fd)
|
|
167
|
+
|
|
168
|
+
def read(self, identifier):
|
|
169
|
+
volumes(identifier)
|
|
170
|
+
try: raw = owned_file(self.fd, identifier + '.json', uid=self.uid)
|
|
171
|
+
except FileNotFoundError: return None
|
|
172
|
+
value = json.loads(raw, object_pairs_hook=unique)
|
|
173
|
+
if not isinstance(value, dict): raise ValueError('Invalid owned state')
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
def write(self, state, *, create=False):
|
|
177
|
+
name = volumes(state['requestId']) and state['requestId'] + '.json'
|
|
178
|
+
raw = encoded(state)
|
|
179
|
+
if len(raw) > 16384: raise ValueError('State exceeds owned bound')
|
|
180
|
+
temporary = name if create else '.write-' + uuid.uuid4().hex
|
|
181
|
+
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=self.fd)
|
|
182
|
+
try:
|
|
183
|
+
with os.fdopen(fd, 'wb') as stream:
|
|
184
|
+
stream.write(raw); stream.flush(); os.fsync(stream.fileno())
|
|
185
|
+
if not create: os.replace(temporary, name, src_dir_fd=self.fd, dst_dir_fd=self.fd)
|
|
186
|
+
os.fsync(self.fd)
|
|
187
|
+
finally:
|
|
188
|
+
if not create:
|
|
189
|
+
try: os.unlink(temporary, dir_fd=self.fd)
|
|
190
|
+
except FileNotFoundError: pass
|
|
191
|
+
|
|
192
|
+
def identifiers(self):
|
|
193
|
+
# State contains metadata only. Tombstones deliberately prevent adoption
|
|
194
|
+
# of a retried/replayed Batch job ID; no age-based source erasure claim.
|
|
195
|
+
return sorted(name[:-5] for name in os.listdir(self.fd) if re.fullmatch(HASH + r'\.json', name))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class UnixConnection(http.client.HTTPConnection):
|
|
199
|
+
def __init__(self, path):
|
|
200
|
+
super().__init__('localhost', timeout=3)
|
|
201
|
+
self.path = path
|
|
202
|
+
def connect(self):
|
|
203
|
+
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
204
|
+
self.sock.settimeout(self.timeout)
|
|
205
|
+
self.sock.connect(self.path)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class Engine:
|
|
209
|
+
def __init__(self, path='/var/run/docker.sock'):
|
|
210
|
+
if not isinstance(path, str) or not path.startswith('/') or not stat.S_ISSOCK(os.stat(path).st_mode):
|
|
211
|
+
raise ValueError('Use the fixed local Docker Engine socket')
|
|
212
|
+
self.path = path
|
|
213
|
+
|
|
214
|
+
def call(self, method, path, body=None, *, statuses=(200,), missing=False, timeout=10):
|
|
215
|
+
if method not in {'GET', 'POST', 'DELETE'} or not path.startswith(API + '/'):
|
|
216
|
+
raise ValueError('Use only the owned native Engine API')
|
|
217
|
+
connection = UnixConnection(self.path)
|
|
218
|
+
started = time.monotonic()
|
|
219
|
+
try:
|
|
220
|
+
raw = None if body is None else encoded(body)
|
|
221
|
+
connection.request(method, path, body=raw, headers={} if raw is None else {'Content-Type': 'application/json'})
|
|
222
|
+
response = connection.getresponse()
|
|
223
|
+
if missing and response.status == 404: return None
|
|
224
|
+
if response.status not in statuses: raise ValueError('Owned Engine operation failed')
|
|
225
|
+
chunks = bytearray()
|
|
226
|
+
while True:
|
|
227
|
+
piece = response.read(16384)
|
|
228
|
+
if not piece: break
|
|
229
|
+
chunks.extend(piece)
|
|
230
|
+
if len(chunks) > MAX_JSON or time.monotonic() - started > timeout:
|
|
231
|
+
raise ValueError('Owned Engine response exceeded bounds')
|
|
232
|
+
if not chunks: return {}
|
|
233
|
+
return json.loads(chunks, object_pairs_hook=unique)
|
|
234
|
+
finally: connection.close()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def cleanup_receipt(state):
|
|
238
|
+
if state.get('phase') != 'cleaned' or not match(state.get('requestId'), HASH) or not match(state.get('owner'), HASH):
|
|
239
|
+
raise ValueError('Only a completed owned cleanup can be acknowledged')
|
|
240
|
+
return {'format': 'enterprise-azure-document-cleanup-v2', 'requestId': state['requestId'],
|
|
241
|
+
'owner': state['owner'], 'nodeId': node_identity(state.get('nodeId')), 'cleaned': True}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class RootReceipts:
|
|
245
|
+
"""Only metadata leaves this root process; no document or result reader.
|
|
246
|
+
|
|
247
|
+
A create-only receipt remains until the generated store's metadata lifecycle
|
|
248
|
+
expires it. This makes a lost acknowledgement repeatable without recreating
|
|
249
|
+
document bytes. VM MI is a node boundary, not a per-process security boundary.
|
|
250
|
+
"""
|
|
251
|
+
def __init__(self, config, node_id, *, connection=None):
|
|
252
|
+
self.config = policy(config)
|
|
253
|
+
self.node_id = node_identity(node_id)
|
|
254
|
+
self.connection = connection
|
|
255
|
+
|
|
256
|
+
def send(self, method, host, path, *, headers=None, data=None):
|
|
257
|
+
connection = (self.connection(method, host, path) if self.connection is not None else
|
|
258
|
+
(http.client.HTTPConnection(host, timeout=3) if host == '169.254.169.254' else
|
|
259
|
+
http.client.HTTPSConnection(host, timeout=3)))
|
|
260
|
+
try:
|
|
261
|
+
connection.request(method, path, body=data, headers={'Accept-Encoding': 'identity', **(headers or {})})
|
|
262
|
+
response = connection.getresponse()
|
|
263
|
+
observed = {}
|
|
264
|
+
for key, value in response.getheaders():
|
|
265
|
+
if key.lower() in observed: raise ValueError()
|
|
266
|
+
observed[key.lower()] = value
|
|
267
|
+
if observed.get('content-encoding') not in {None, 'identity'}: raise ValueError()
|
|
268
|
+
started = time.monotonic(); raw = bytearray()
|
|
269
|
+
while True:
|
|
270
|
+
chunk = response.read(4096)
|
|
271
|
+
if not chunk: break
|
|
272
|
+
raw.extend(chunk)
|
|
273
|
+
if len(raw) > 32768 or time.monotonic() - started > 8: raise ValueError()
|
|
274
|
+
return response.status, observed, bytes(raw)
|
|
275
|
+
finally: connection.close()
|
|
276
|
+
|
|
277
|
+
def publish(self, receipt):
|
|
278
|
+
if not isinstance(receipt, dict) or receipt.get('cleaned') is not True:
|
|
279
|
+
raise ValueError('Use the exact metadata-only owned cleanup receipt')
|
|
280
|
+
expected = {'format': 'enterprise-azure-document-cleanup-v2', 'requestId': receipt.get('requestId'),
|
|
281
|
+
'owner': owner(self.config), 'nodeId': self.node_id, 'cleaned': True}
|
|
282
|
+
if receipt != expected or not match(receipt.get('requestId'), HASH):
|
|
283
|
+
raise ValueError('Use the exact metadata-only owned cleanup receipt')
|
|
284
|
+
try:
|
|
285
|
+
path = '/metadata/identity/oauth2/token?' + urlencode({'api-version': '2018-02-01',
|
|
286
|
+
'resource': 'https://storage.azure.com/', 'client_id': self.config['transferClientId']})
|
|
287
|
+
status, _, raw = self.send('GET', '169.254.169.254', path, headers={'Metadata': 'true'})
|
|
288
|
+
if status != 200: raise ValueError()
|
|
289
|
+
access = json.loads(raw, object_pairs_hook=unique)
|
|
290
|
+
token = access.get('access_token')
|
|
291
|
+
if (access.get('token_type') != 'Bearer' or access.get('resource') != 'https://storage.azure.com/' or
|
|
292
|
+
not match(access.get('expires_on'), '[1-9][0-9]{0,11}') or int(access['expires_on']) < time.time() + 30 or
|
|
293
|
+
not isinstance(token, str) or not 8 <= len(token) <= 20000 or any(ord(c) < 33 or ord(c) > 126 for c in token)):
|
|
294
|
+
raise ValueError()
|
|
295
|
+
host = self.config['storageAccount'] + '.blob.core.windows.net'
|
|
296
|
+
path = '/' + self.config['container'] + '/document-jobs/' + receipt['requestId'] + '/cleanup/' + self.node_id
|
|
297
|
+
raw = encoded(receipt)
|
|
298
|
+
common = {'Authorization': 'Bearer ' + token, 'x-ms-version': '2025-05-05',
|
|
299
|
+
'x-ms-date': format_datetime(datetime.now(timezone.utc), usegmt=True)}
|
|
300
|
+
status, headers, _ = self.send('PUT', host, path, data=raw, headers={**common,
|
|
301
|
+
'If-None-Match': '*', 'x-ms-blob-type': 'BlockBlob', 'Content-Type': 'application/json',
|
|
302
|
+
'Content-Length': str(len(raw)),
|
|
303
|
+
'Content-MD5': base64.b64encode(hashlib.md5(raw, usedforsecurity=False).digest()).decode()})
|
|
304
|
+
if status == 201:
|
|
305
|
+
if (headers.get('x-ms-request-server-encrypted') != 'true' or headers.get('x-ms-version-id') is not None or
|
|
306
|
+
not match(headers.get('etag'), r'"0x[0-9A-Fa-f]{1,32}"')):
|
|
307
|
+
raise ValueError()
|
|
308
|
+
return
|
|
309
|
+
if status != 412: raise ValueError()
|
|
310
|
+
# A lost PUT response may have committed the exact acknowledgement.
|
|
311
|
+
# Never overwrite or adopt a different owner's receipt.
|
|
312
|
+
status, headers, observed = self.send('GET', host, path, headers=common)
|
|
313
|
+
if (status != 200 or headers.get('x-ms-server-encrypted') != 'true' or
|
|
314
|
+
headers.get('x-ms-blob-type') != 'BlockBlob' or headers.get('x-ms-version-id') is not None or
|
|
315
|
+
headers.get('content-length') != str(len(observed)) or observed != raw):
|
|
316
|
+
raise ValueError()
|
|
317
|
+
except Exception:
|
|
318
|
+
raise ValueError('Owned cleanup acknowledgement remains pending') from None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class Host:
|
|
322
|
+
def __init__(self, config, store, engine, *, node_id, receipts, clock=time.time, monotonic=time.monotonic, sleep=time.sleep):
|
|
323
|
+
self.config, self.store, self.engine = policy(config), store, engine
|
|
324
|
+
self.node_id = node_identity(node_id)
|
|
325
|
+
self.clock, self.monotonic, self.sleep = clock, monotonic, sleep
|
|
326
|
+
if not callable(getattr(receipts, 'publish', None)): raise ValueError('Use an owned cleanup receipt publisher')
|
|
327
|
+
self.receipts = receipts
|
|
328
|
+
self.owner = owner(self.config)
|
|
329
|
+
|
|
330
|
+
def check_state(self, state):
|
|
331
|
+
if isinstance(state, dict) and state.get('format') == 'enterprise-azure-document-host-v1' and set(state) == STATE_FIELDS - {'receiptPublished', 'nodeId'}:
|
|
332
|
+
state.update(format='enterprise-azure-document-host-v2', receiptPublished=False)
|
|
333
|
+
if isinstance(state, dict) and state.get('format') == 'enterprise-azure-document-host-v2' and set(state) == STATE_FIELDS - {'nodeId'}:
|
|
334
|
+
# A legacy global receipt did not settle every possible retry node.
|
|
335
|
+
# Reinspect this local node and publish its own scoped receipt.
|
|
336
|
+
state.update(format='enterprise-azure-document-host-v3', nodeId=self.node_id, receiptPublished=False)
|
|
337
|
+
if (not isinstance(state, dict) or set(state) != STATE_FIELDS or state['format'] != 'enterprise-azure-document-host-v3' or
|
|
338
|
+
state['nodeId'] != self.node_id or
|
|
339
|
+
not match(state['requestId'], HASH) or not match(state['nonce'], '[0-9a-f]{32}') or
|
|
340
|
+
state['owner'] != self.owner or not match(state['policy'], HASH) or not match(state['helperSha256'], HASH) or
|
|
341
|
+
type(state['createdAt']) is not int or type(state['expiresAt']) is not int or
|
|
342
|
+
state['expiresAt'] - state['createdAt'] != 600 or type(state['published']) is not bool or type(state['receiptPublished']) is not bool or
|
|
343
|
+
(state['receiptPublished'] and state['phase'] != 'cleaned') or
|
|
344
|
+
state['phase'] not in {'preparing', 'ready', 'releasing', 'cleanup_failed', 'cleaned'}):
|
|
345
|
+
raise ValueError('Unowned or invalid recovery state')
|
|
346
|
+
for key in ['parserImage', 'transferImage']:
|
|
347
|
+
if image(state[key]).split('/')[0] != self.config['parserImage'].split('/')[0]:
|
|
348
|
+
raise ValueError('Recovery artifact registry changed')
|
|
349
|
+
transfer(state['transfer'], self.config)
|
|
350
|
+
if state['transfer']['request']['requestId'] != state['requestId']:
|
|
351
|
+
raise ValueError('Recovery request identity changed')
|
|
352
|
+
return state
|
|
353
|
+
|
|
354
|
+
def labels(self, state):
|
|
355
|
+
return {'atmem.document.owner': state['owner'], 'atmem.document.request': state['requestId'],
|
|
356
|
+
'atmem.document.policy': state['policy']}
|
|
357
|
+
|
|
358
|
+
def volume_labels(self, state, side):
|
|
359
|
+
return {**self.labels(state), 'atmem.document.nonce': state['nonce'], 'atmem.document.side': side}
|
|
360
|
+
|
|
361
|
+
def volume(self, state, side, *, missing=False):
|
|
362
|
+
name = volumes(state['requestId'])[0 if side == 'input' else 1]
|
|
363
|
+
info = self.engine.call('GET', API + '/volumes/' + quote(name, safe=''), missing=missing)
|
|
364
|
+
if info is None: return None
|
|
365
|
+
if (info.get('Name') != name or info.get('Driver') != 'local' or (info.get('Options') is not None and info.get('Options') != {}) or
|
|
366
|
+
info.get('Labels') != self.volume_labels(state, side)):
|
|
367
|
+
raise ValueError('Do not adopt or delete an unowned volume')
|
|
368
|
+
return info
|
|
369
|
+
|
|
370
|
+
def preflight(self):
|
|
371
|
+
info = self.engine.call('GET', API + '/info')
|
|
372
|
+
if (info.get('OSType') != 'linux' or info.get('LiveRestoreEnabled') is not False or
|
|
373
|
+
info.get('LoggingDriver') != 'none' or
|
|
374
|
+
not any(item.startswith('name=seccomp,profile=builtin') for item in info.get('SecurityOptions', []))):
|
|
375
|
+
raise ValueError('Use the reviewed dedicated no-log/non-live-restore Engine')
|
|
376
|
+
for key in ['parserImage', 'transferImage']:
|
|
377
|
+
info = self.engine.call('GET', API + '/images/' + quote(self.config[key], safe='') + '/json')
|
|
378
|
+
if (self.config[key] not in info.get('RepoDigests', []) or info.get('Os') != 'linux' or
|
|
379
|
+
info.get('Architecture') != 'amd64' or info.get('Config', {}).get('User') != '10001:10001'):
|
|
380
|
+
raise ValueError('Owned immutable artifact is not prefetched')
|
|
381
|
+
|
|
382
|
+
def container(self, state, identifier):
|
|
383
|
+
if not match(identifier, HASH): raise ValueError('Invalid native container ID')
|
|
384
|
+
info = self.engine.call('GET', API + '/containers/' + identifier + '/json', missing=True)
|
|
385
|
+
if info is None: return None
|
|
386
|
+
config, host = info['Config'], info['HostConfig']
|
|
387
|
+
labels = config.get('Labels') or {}
|
|
388
|
+
stage = labels.get('atmem.document.stage')
|
|
389
|
+
if stage not in {'parser', 'download', 'upload'} or any(labels.get(k) != v for k, v in self.labels(state).items()):
|
|
390
|
+
raise ValueError('Foreign container uses an owned volume')
|
|
391
|
+
expected_image = state['parserImage'] if stage == 'parser' else state['transferImage']
|
|
392
|
+
if config.get('Image') != expected_image:
|
|
393
|
+
raise ValueError('Container artifact ownership changed')
|
|
394
|
+
if ((stage == 'parser' and not match(config.get('User'), r'[1-9][0-9]{0,9}(?::[0-9]{1,10})?')) or
|
|
395
|
+
(stage != 'parser' and config.get('User') != '0:0')):
|
|
396
|
+
raise ValueError('Container execution user changed')
|
|
397
|
+
if stage != 'parser' and labels.get('atmem.document.nonce') != state['nonce']:
|
|
398
|
+
raise ValueError('Transfer container ownership changed')
|
|
399
|
+
mounts = info.get('Mounts', [])
|
|
400
|
+
if len(mounts) != 2: raise ValueError('Unexpected host or data mount')
|
|
401
|
+
for side, name in zip(['input', 'output'], volumes(state['requestId'])):
|
|
402
|
+
entries = [m for m in mounts if m.get('Destination') == '/' + side]
|
|
403
|
+
writable = stage == 'download' or stage == 'parser' and side == 'output'
|
|
404
|
+
if len(entries) != 1 or entries[0].get('Type') != 'volume' or entries[0].get('Name') != name or entries[0].get('RW') is not writable:
|
|
405
|
+
raise ValueError('Per-file mount ownership changed')
|
|
406
|
+
if (host.get('NetworkMode') != ('none' if stage == 'parser' else 'host') or
|
|
407
|
+
host.get('ReadonlyRootfs') is not True or host.get('Privileged') is not False or
|
|
408
|
+
host.get('CapDrop') != ['ALL'] or
|
|
409
|
+
(host.get('CapAdd') or []) != (['DAC_READ_SEARCH'] if stage == 'upload' else []) or
|
|
410
|
+
host.get('PidMode') not in {'', None} or host.get('IpcMode') not in {'private', ''} or
|
|
411
|
+
host.get('Devices') or host.get('LogConfig', {}).get('Type') != 'none' or
|
|
412
|
+
'no-new-privileges:true' not in (host.get('SecurityOpt') or [])):
|
|
413
|
+
raise ValueError('Container isolation changed; refuse unowned cleanup')
|
|
414
|
+
return info
|
|
415
|
+
|
|
416
|
+
def containers(self, state):
|
|
417
|
+
identifiers = set()
|
|
418
|
+
for name in volumes(state['requestId']):
|
|
419
|
+
filters = json.dumps({'volume': [name]}, separators=(',', ':'))
|
|
420
|
+
result = self.engine.call('GET', API + '/containers/json?' + urlencode({'all': '1', 'filters': filters}))
|
|
421
|
+
if not isinstance(result, list) or len(result) > 32: raise ValueError('Unexpected owned volume consumers')
|
|
422
|
+
for item in result:
|
|
423
|
+
if not match(item.get('Id'), HASH): raise ValueError('Invalid native consumer identity')
|
|
424
|
+
identifiers.add(item['Id'])
|
|
425
|
+
# Validate every consumer before deleting any container or volume.
|
|
426
|
+
return [info for identifier in sorted(identifiers) if (info := self.container(state, identifier)) is not None]
|
|
427
|
+
|
|
428
|
+
def cleanup(self, state):
|
|
429
|
+
self.check_state(state)
|
|
430
|
+
for side in ['input', 'output']: self.volume(state, side, missing=True)
|
|
431
|
+
for info in self.containers(state):
|
|
432
|
+
self.engine.call('DELETE', API + '/containers/' + info['Id'] + '?force=true', statuses=(204,), missing=True)
|
|
433
|
+
for side in ['output', 'input']:
|
|
434
|
+
found = self.volume(state, side, missing=True)
|
|
435
|
+
if found is not None:
|
|
436
|
+
self.engine.call('DELETE', API + '/volumes/' + quote(found['Name'], safe=''), statuses=(204,), missing=True)
|
|
437
|
+
state['phase'] = 'cleaned'
|
|
438
|
+
self.store.write(state)
|
|
439
|
+
if not state['receiptPublished']:
|
|
440
|
+
self.receipts.publish(cleanup_receipt(state))
|
|
441
|
+
state['receiptPublished'] = True
|
|
442
|
+
self.store.write(state)
|
|
443
|
+
|
|
444
|
+
def run_transfer(self, state, stage):
|
|
445
|
+
if stage not in {'download', 'upload'}: raise ValueError('Use a trusted transfer stage')
|
|
446
|
+
labels = {**self.labels(state), 'atmem.document.nonce': state['nonce'], 'atmem.document.stage': stage}
|
|
447
|
+
mounts = [{'Type': 'volume', 'Source': name, 'Target': '/' + side, 'ReadOnly': stage == 'upload'}
|
|
448
|
+
for side, name in zip(['input', 'output'], volumes(state['requestId']))]
|
|
449
|
+
body = {'Image': state['transferImage'], 'User': '0:0',
|
|
450
|
+
'Cmd': ['python', '-I', '/app/enterprise_azure_transfer.py', stage],
|
|
451
|
+
'Env': ['DOCUMENT_JOB=' + encoded(state['transfer']).decode()], 'Labels': labels,
|
|
452
|
+
'HostConfig': {'NetworkMode': 'host', 'ReadonlyRootfs': True, 'Privileged': False,
|
|
453
|
+
'CapDrop': ['ALL'], 'CapAdd': ['DAC_READ_SEARCH'] if stage == 'upload' else [],
|
|
454
|
+
'SecurityOpt': ['no-new-privileges:true'], 'LogConfig': {'Type': 'none', 'Config': {}},
|
|
455
|
+
'PidsLimit': 64, 'Memory': 268435456, 'MemorySwap': 268435456, 'NanoCpus': 1000000000,
|
|
456
|
+
'Mounts': mounts, 'Tmpfs': {'/tmp': 'rw,noexec,nosuid,nodev,size=32m'}, 'RestartPolicy': {'Name': 'no'}}}
|
|
457
|
+
name = 'atmem-document-' + state['requestId'] + '-' + stage
|
|
458
|
+
reply = self.engine.call('POST', API + '/containers/create?' + urlencode({'name': name, 'platform': 'linux/amd64'}), body, statuses=(201,))
|
|
459
|
+
identifier = reply.get('Id')
|
|
460
|
+
if not match(identifier, HASH): raise ValueError('Invalid native transfer acknowledgement')
|
|
461
|
+
self.container(state, identifier)
|
|
462
|
+
self.engine.call('POST', API + '/containers/' + identifier + '/start', statuses=(204,))
|
|
463
|
+
deadline = self.monotonic() + 35
|
|
464
|
+
while True:
|
|
465
|
+
observed = self.container(state, identifier)
|
|
466
|
+
if observed is None: raise ValueError('Transfer process disappeared')
|
|
467
|
+
if not observed['State']['Running']:
|
|
468
|
+
if observed['State'].get('ExitCode') != 0 or observed['State'].get('OOMKilled'):
|
|
469
|
+
raise ValueError('Trusted transfer failed')
|
|
470
|
+
self.engine.call('DELETE', API + '/containers/' + identifier, statuses=(204,))
|
|
471
|
+
return
|
|
472
|
+
if self.monotonic() >= deadline: raise ValueError('Trusted transfer exceeded deadline')
|
|
473
|
+
self.sleep(.1)
|
|
474
|
+
|
|
475
|
+
def prepare(self, value):
|
|
476
|
+
value = transfer(value, self.config)
|
|
477
|
+
identifier = value['request']['requestId']
|
|
478
|
+
with self.store.lock(identifier):
|
|
479
|
+
if self.store.read(identifier) is not None: raise ValueError('Do not replay an existing per-file job')
|
|
480
|
+
self.preflight()
|
|
481
|
+
now = int(self.clock())
|
|
482
|
+
state = {'format': 'enterprise-azure-document-host-v3', 'requestId': identifier, 'nodeId': self.node_id, 'nonce': uuid.uuid4().hex,
|
|
483
|
+
'owner': self.owner, 'policy': digest(self.config), 'helperSha256': self.config['hostHelperSha256'],
|
|
484
|
+
'parserImage': self.config['parserImage'], 'transferImage': self.config['transferImage'], 'transfer': value,
|
|
485
|
+
'createdAt': now, 'expiresAt': now + 600, 'phase': 'preparing', 'published': False, 'receiptPublished': False}
|
|
486
|
+
# Durable intent exists before the first volume/container creation.
|
|
487
|
+
self.store.write(state, create=True)
|
|
488
|
+
try:
|
|
489
|
+
for side, name in zip(['input', 'output'], volumes(identifier)):
|
|
490
|
+
if self.engine.call('GET', API + '/volumes/' + quote(name, safe=''), missing=True) is not None:
|
|
491
|
+
raise ValueError('Do not adopt an existing volume')
|
|
492
|
+
self.engine.call('POST', API + '/volumes/create', {'Name': name, 'Driver': 'local',
|
|
493
|
+
'Labels': self.volume_labels(state, side)}, statuses=(201,))
|
|
494
|
+
self.volume(state, side)
|
|
495
|
+
self.run_transfer(state, 'download')
|
|
496
|
+
state['phase'] = 'ready'
|
|
497
|
+
self.store.write(state)
|
|
498
|
+
except BaseException:
|
|
499
|
+
try: self.cleanup(state)
|
|
500
|
+
except Exception:
|
|
501
|
+
if state['phase'] != 'cleaned': state['phase'] = 'cleanup_failed'
|
|
502
|
+
self.store.write(state)
|
|
503
|
+
raise
|
|
504
|
+
|
|
505
|
+
def release(self, value, transfer_image):
|
|
506
|
+
value = transfer(value, self.config)
|
|
507
|
+
identifier = value['request']['requestId']
|
|
508
|
+
with self.store.lock(identifier):
|
|
509
|
+
state = self.store.read(identifier)
|
|
510
|
+
if state is None: raise ValueError('No owned per-file preparation')
|
|
511
|
+
self.check_state(state)
|
|
512
|
+
if state['transfer'] != value or state['transferImage'] != transfer_image:
|
|
513
|
+
raise ValueError('Release must match the original per-file owner')
|
|
514
|
+
if state['phase'] == 'cleaned':
|
|
515
|
+
if not state['receiptPublished']: self.cleanup(state)
|
|
516
|
+
if not state['published']: raise ValueError('Failed job was cleaned without publication')
|
|
517
|
+
return
|
|
518
|
+
succeeded = False
|
|
519
|
+
try:
|
|
520
|
+
if state['phase'] != 'ready': raise ValueError('Preparation never completed')
|
|
521
|
+
if any(info['State']['Running'] for info in self.containers(state)):
|
|
522
|
+
raise ValueError('A parser or transfer still owns the file')
|
|
523
|
+
state['phase'] = 'releasing'; self.store.write(state)
|
|
524
|
+
self.run_transfer(state, 'upload')
|
|
525
|
+
state['published'] = True; self.store.write(state)
|
|
526
|
+
succeeded = True
|
|
527
|
+
finally:
|
|
528
|
+
try: self.cleanup(state)
|
|
529
|
+
except Exception:
|
|
530
|
+
if state['phase'] != 'cleaned': state['phase'] = 'cleanup_failed'
|
|
531
|
+
self.store.write(state)
|
|
532
|
+
raise
|
|
533
|
+
if not succeeded: raise ValueError('Document publication did not complete')
|
|
534
|
+
|
|
535
|
+
def reap(self):
|
|
536
|
+
failures = []
|
|
537
|
+
for identifier in self.store.identifiers():
|
|
538
|
+
try:
|
|
539
|
+
with self.store.lock(identifier):
|
|
540
|
+
state = self.check_state(self.store.read(identifier))
|
|
541
|
+
if ((state['phase'] == 'cleaned' and not state['receiptPublished']) or
|
|
542
|
+
state['phase'] != 'cleaned' and state['expiresAt'] <= int(self.clock())):
|
|
543
|
+
self.cleanup(state)
|
|
544
|
+
except BlockingIOError:
|
|
545
|
+
continue # An active trusted prepare/release holds this lock.
|
|
546
|
+
except Exception:
|
|
547
|
+
failures.append(identifier)
|
|
548
|
+
if failures: raise ValueError('Owned document cleanup remains pending')
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def main():
|
|
552
|
+
store = None
|
|
553
|
+
try:
|
|
554
|
+
if os.geteuid() != 0 or len(sys.argv) != 2 or sys.argv[1] not in {'prepare', 'release', 'reap'}:
|
|
555
|
+
raise ValueError('Use the fixed root-owned host command')
|
|
556
|
+
directory = protected_directory('/etc/atmem')
|
|
557
|
+
try:
|
|
558
|
+
config = policy(json.loads(owned_file(directory, 'document-host.json'), object_pairs_hook=unique))
|
|
559
|
+
node = json.loads(owned_file(directory, 'document-host-node.json'), object_pairs_hook=unique)
|
|
560
|
+
if not isinstance(node, dict) or set(node) != {'format', 'nodeId'} or node['format'] != 'enterprise-azure-document-node-v1':
|
|
561
|
+
raise ValueError('Use the protected instance-specific native node identity')
|
|
562
|
+
node_id = node_identity(node['nodeId'])
|
|
563
|
+
finally: os.close(directory)
|
|
564
|
+
directory = protected_directory('/var/lib/atmem/document-jobs')
|
|
565
|
+
try: store = Store(directory)
|
|
566
|
+
finally: os.close(directory)
|
|
567
|
+
host = Host(config, store, Engine(), node_id=node_id, receipts=RootReceipts(config, node_id))
|
|
568
|
+
mode = sys.argv[1]
|
|
569
|
+
# The root service may invoke reap directly, without the inline Batch
|
|
570
|
+
# bootstrap. Verify this installed standalone helper in both paths.
|
|
571
|
+
helper_dir = protected_directory(str(Path(__file__).parent))
|
|
572
|
+
try: helper_sha = hashlib.sha256(owned_file(helper_dir, Path(__file__).name, limit=262144)).hexdigest()
|
|
573
|
+
finally: os.close(helper_dir)
|
|
574
|
+
if mode in {'prepare', 'reap'} and helper_sha != config['hostHelperSha256']:
|
|
575
|
+
raise ValueError('Installed helper differs from the protected pool policy')
|
|
576
|
+
if mode == 'reap': host.reap(); return
|
|
577
|
+
value = os.environ.get('DOCUMENT_JOB', '')
|
|
578
|
+
if not 0 < len(value.encode()) <= 4096: raise ValueError('Use the owned per-file metadata')
|
|
579
|
+
value = transfer(json.loads(value, object_pairs_hook=unique), config)
|
|
580
|
+
if (os.environ.get('AZ_BATCH_JOB_ID') != value['request']['requestId'] or
|
|
581
|
+
os.environ.get('AZ_BATCH_POOL_ID') != config['poolId'] or
|
|
582
|
+
os.environ.get('AZ_BATCH_ACCOUNT_NAME') != config['batchAccount'] or os.environ.get('AZ_BATCH_NODE_ID') != node_id):
|
|
583
|
+
raise ValueError('Native Batch owner changed')
|
|
584
|
+
if mode == 'prepare':
|
|
585
|
+
if os.environ.get('DOCUMENT_TRANSFER_IMAGE') != config['transferImage']:
|
|
586
|
+
raise ValueError('Transfer artifact differs from the protected pool policy')
|
|
587
|
+
host.prepare(value)
|
|
588
|
+
else:
|
|
589
|
+
existing = store.read(value['request']['requestId'])
|
|
590
|
+
if existing is None or existing.get('helperSha256') != helper_sha:
|
|
591
|
+
raise ValueError('Release helper differs from the original per-file owner')
|
|
592
|
+
host.release(value, os.environ.get('DOCUMENT_TRANSFER_IMAGE'))
|
|
593
|
+
except Exception:
|
|
594
|
+
raise SystemExit('Owned Azure document host operation could not complete; recovery remains pending.') from None
|
|
595
|
+
finally:
|
|
596
|
+
if store is not None: store.close()
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
if __name__ == '__main__': main()
|