@atmem/architect 0.0.0-stage → 0.2.0-alpha.4.1
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/README.md +33 -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-B97MkVMs.css +1 -0
- package/dist/assets/index-qtCM2D7O.js +21271 -0
- package/dist/assets/index.es-C6C0sneB.js +18 -0
- package/dist/assets/purify.es-CAi2OJFm.js +3 -0
- package/dist/assets/visual-export-DjbjgQbA.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/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/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 +170 -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 +215 -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 +163 -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 +469 -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 +181 -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_routes.py +82 -0
- package/server/memory_service.py +273 -0
- package/server/memory_worker.py +66 -0
- package/server/openai_voice.py +80 -0
- package/server/packaged_app.py +9 -0
- package/server/pricing.py +84 -0
- package/server/process_control_observation.py +31 -0
- package/server/prompts.py +123 -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/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 +46 -0
- package/src/ArtifactChecks.jsx +49 -0
- package/src/BlockRequirements.jsx +13 -0
- package/src/BrainActivity.jsx +11 -0
- package/src/ChatMessage.jsx +19 -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 +385 -0
- package/src/EngineeringPanel.jsx +151 -0
- package/src/ImplementationDelivery.jsx +55 -0
- package/src/JourneyViews.jsx +400 -0
- package/src/MemoryPanel.jsx +46 -0
- package/src/OpenAIVoicePanel.jsx +65 -0
- package/src/OutputsView.jsx +65 -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 +17 -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 +188 -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/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 +72 -0
- package/src/journey.mjs +87 -0
- package/src/levels.mjs +145 -0
- package/src/local-agent.mjs +184 -0
- package/src/main.jsx +2458 -0
- package/src/model-policy-preflight.mjs +34 -0
- package/src/openai-voice.mjs +102 -0
- package/src/output-bundle.mjs +11 -0
- package/src/ownership-inspection.mjs +28 -0
- package/src/pack.mjs +201 -0
- package/src/pricing.mjs +31 -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 +36 -0
- package/src/useEmulation.jsx +36 -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,105 @@
|
|
|
1
|
+
"""Fixed SSM host inspection. Standard library only; no document or secret input."""
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
import ipaddress
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import shlex
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(argv):
|
|
13
|
+
if argv and argv[0]=='/usr/bin/docker':
|
|
14
|
+
# An operator's saved Docker context must never redirect this native
|
|
15
|
+
# host inspection to another daemon, even with a scrubbed environment.
|
|
16
|
+
argv=[argv[0],'--host','unix:///var/run/docker.sock',*argv[1:]]
|
|
17
|
+
value=subprocess.run(argv,stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,
|
|
18
|
+
timeout=4,check=True,env={'PATH':'/usr/sbin:/usr/bin:/sbin:/bin','LANG':'C'})
|
|
19
|
+
if len(value.stdout)>32768:raise ValueError('Invalid host observation')
|
|
20
|
+
return value.stdout.decode().strip()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def expected_policy(expected):
|
|
24
|
+
if (not isinstance(expected,dict)or set(expected)!={'format','clusterArn','policySha256'}or
|
|
25
|
+
expected['format']!='enterprise-aws-host-policy-v1'or
|
|
26
|
+
not isinstance(expected['policySha256'],str)or not re.fullmatch('[0-9a-f]{64}',expected['policySha256'])or
|
|
27
|
+
not isinstance(expected['clusterArn'],str)or not re.fullmatch(r'arn:aws:ecs:[a-z]{2}-[a-z]+-[1-9]:[0-9]{12}:cluster/[A-Za-z0-9_-]{1,255}',expected['clusterArn'])):
|
|
28
|
+
raise ValueError('Use the exact owned host policy')
|
|
29
|
+
return dict(expected)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def first_rule_matches(lines,chain,pairs):
|
|
33
|
+
active=[value for value in lines.splitlines()if value.startswith('-A ')]
|
|
34
|
+
if not active:return False
|
|
35
|
+
fields=shlex.split(active[0])
|
|
36
|
+
if fields[:2]!=['-A',chain]or len(fields)!=2+2*len(pairs):return False
|
|
37
|
+
actual={}
|
|
38
|
+
for index in range(2,len(fields),2):
|
|
39
|
+
key,value=fields[index:index+2]
|
|
40
|
+
if key in actual:return False
|
|
41
|
+
if key=='-d':
|
|
42
|
+
try:value=str(ipaddress.ip_network(value,strict=True))
|
|
43
|
+
except ValueError:return False
|
|
44
|
+
actual[key]=value
|
|
45
|
+
return actual==pairs
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def inspect_host(expected,*,read=lambda name:Path(name).read_text(),command=run):
|
|
49
|
+
expected=expected_policy(expected)
|
|
50
|
+
# Parse values as inert text. Never source a host config into a shell.
|
|
51
|
+
values={}
|
|
52
|
+
for line in read('/etc/ecs/ecs.config').splitlines():
|
|
53
|
+
if not line.strip()or line.lstrip().startswith('#'):continue
|
|
54
|
+
key,separator,value=line.partition('=')
|
|
55
|
+
if not separator or key in values:raise ValueError('Ambiguous ECS configuration')
|
|
56
|
+
values[key]=value
|
|
57
|
+
if (values.get('ECS_CLUSTER')!=expected['clusterArn']or
|
|
58
|
+
values.get('ECS_ENABLE_TASK_IAM_ROLE')!='true'or
|
|
59
|
+
values.get('ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION')!='1s'or
|
|
60
|
+
values.get('ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST')!='false'):
|
|
61
|
+
raise ValueError('ECS host policy changed')
|
|
62
|
+
attributes=json.loads(values.get('ECS_INSTANCE_ATTRIBUTES','{}'))
|
|
63
|
+
if attributes!={'atmem-document-policy':expected['policySha256']}:raise ValueError('Host policy selection changed')
|
|
64
|
+
daemon=json.loads(read('/etc/docker/daemon.json'))
|
|
65
|
+
if (daemon.get('log-driver')!='none'or daemon.get('live-restore',False)is not False or daemon.get('bridge','docker0')!='docker0'):
|
|
66
|
+
raise ValueError('Unsafe daemon logging, network or task persistence')
|
|
67
|
+
if json.loads(command(['/usr/bin/docker','info','--format','{{json .LoggingDriver}}']))!='none':raise ValueError('Actual daemon logging changed')
|
|
68
|
+
if json.loads(command(['/usr/bin/docker','info','--format','{{json .LiveRestoreEnabled}}']))is not False:raise ValueError('Native live restore is active')
|
|
69
|
+
options=json.loads(command(['/usr/bin/docker','info','--format','{{json .SecurityOptions}}']))
|
|
70
|
+
if not isinstance(options,list)or 'name=seccomp,profile=builtin'not in options:
|
|
71
|
+
raise ValueError('Native seccomp is not active')
|
|
72
|
+
bridge=json.loads(command(['/usr/bin/docker','network','inspect','bridge','--format','{{json .Options}}']))
|
|
73
|
+
if bridge.get('com.docker.network.bridge.name')!='docker0':raise ValueError('Actual default bridge changed')
|
|
74
|
+
native_env=json.loads(command(['/usr/bin/docker','inspect','ecs-agent','--format','{{json .Config.Env}}']))
|
|
75
|
+
if not isinstance(native_env,list)or any(not isinstance(v,str)for v in native_env):raise ValueError('Native ECS agent configuration unavailable')
|
|
76
|
+
native={}
|
|
77
|
+
for entry in native_env:
|
|
78
|
+
key,separator,value=entry.partition('=')
|
|
79
|
+
if not separator or key in native:raise ValueError('Ambiguous native ECS environment')
|
|
80
|
+
native[key]=value
|
|
81
|
+
for key in ['ECS_CLUSTER','ECS_ENABLE_TASK_IAM_ROLE','ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION','ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST','ECS_INSTANCE_ATTRIBUTES']:
|
|
82
|
+
if native.get(key)!=values[key]:raise ValueError('Running ECS agent differs from host policy')
|
|
83
|
+
# Both paths are fixed, read-only probes. A plain TCP bridge must not expose
|
|
84
|
+
# instance-profile credentials to the transfer containers.
|
|
85
|
+
for destination in ['169.254.169.254/32','fd00:ec2::254/128']:
|
|
86
|
+
tool='/usr/sbin/iptables'if ':'not in destination else'/usr/sbin/ip6tables'
|
|
87
|
+
rules=command([tool,'-w','2','-S','DOCKER-USER'])
|
|
88
|
+
if not first_rule_matches(rules,'DOCKER-USER',{'-i':'docker0','-d':destination,'-j':'DROP'}):raise ValueError('IMDS rule is missing or shadowed')
|
|
89
|
+
forward=command([tool,'-w','2','-S','FORWARD'])
|
|
90
|
+
if not first_rule_matches(forward,'FORWARD',{'-j':'DOCKER-USER'}):raise ValueError('IMDS chain is not the first forwarding hook')
|
|
91
|
+
boot=read('/proc/sys/kernel/random/boot_id').strip()
|
|
92
|
+
if not re.fullmatch('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',boot):raise ValueError('Invalid native boot identity')
|
|
93
|
+
return {'format':'enterprise-aws-host-observation-v1','policySha256':expected['policySha256'],
|
|
94
|
+
'clusterArn':expected['clusterArn'],'bootId':boot}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
try:
|
|
99
|
+
if len(sys.argv)!=2 or len(sys.argv[1])>4096:raise ValueError()
|
|
100
|
+
expected=json.loads(base64.b64decode(sys.argv[1],validate=True))
|
|
101
|
+
print(json.dumps(inspect_host(expected),separators=(',',':')))
|
|
102
|
+
except Exception:raise SystemExit(2)from None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__=='__main__':main()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Point-in-time host inspection through one fixed, hash/version-bound SSM document."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
from enterprise_sources import SourceUnavailable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def unique(pairs):
|
|
16
|
+
value={}
|
|
17
|
+
for key,item in pairs:
|
|
18
|
+
if key in value:raise ValueError('Ambiguous host-inspection content')
|
|
19
|
+
value[key]=item
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def command_document(config):
|
|
24
|
+
source=Path(__file__).with_name('enterprise_aws_host_probe.py').read_bytes()
|
|
25
|
+
expected={'format':'enterprise-aws-host-policy-v1','clusterArn':config['clusterArn'],'policySha256':config['hostPolicySha256']}
|
|
26
|
+
launcher='import base64;exec(compile(base64.b64decode('+repr(base64.b64encode(source).decode())+'),"owned-host-probe","exec"))'
|
|
27
|
+
command='exec /usr/bin/python3 -I -c '+shlex.quote(launcher)+' '+shlex.quote(base64.b64encode(json.dumps(expected,sort_keys=True,separators=(',',':')).encode()).decode())
|
|
28
|
+
return {'schemaVersion':'2.2','description':'Fixed read-only parser host inspection; no caller command or secret parameters.',
|
|
29
|
+
'parameters':{},'mainSteps':[{'action':'aws:runShellScript','name':'verifyHost',
|
|
30
|
+
'precondition':{'StringEquals':['platformType','Linux']},'inputs':{'timeoutSeconds':'30','runCommand':[command]}}]}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class VerifiedHosts:
|
|
35
|
+
policy_sha256:str
|
|
36
|
+
members:tuple
|
|
37
|
+
expires_at:float
|
|
38
|
+
|
|
39
|
+
def placement(self,config):
|
|
40
|
+
if (self.policy_sha256!=config['hostPolicySha256']or type(self.expires_at)not in {int,float}or not math.isfinite(self.expires_at)or
|
|
41
|
+
not 0<self.expires_at-time.monotonic()<=121 or type(self.members)is not tuple or not 1<=len(self.members)<=10):
|
|
42
|
+
raise SourceUnavailable('document_host_verification_changed')
|
|
43
|
+
prefix='arn:aws:ecs:'+config['region']+':'+config['accountId']+':container-instance/'+config['clusterArn'].rsplit('/',1)[1]+'/'
|
|
44
|
+
instances=[];arns=[]
|
|
45
|
+
for member in self.members:
|
|
46
|
+
if (type(member)is not tuple or len(member)!=2 or not isinstance(member[0],str)or
|
|
47
|
+
not re.fullmatch(re.escape(prefix)+'[0-9a-f]{32}',member[0])or not isinstance(member[1],str)or not re.fullmatch('i-[0-9a-f]{17}',member[1])):
|
|
48
|
+
raise SourceUnavailable('document_host_verification_changed')
|
|
49
|
+
arns.append(member[0]);instances.append(member[1])
|
|
50
|
+
if len(set(instances))!=len(instances)or len(set(arns))!=len(arns):raise SourceUnavailable('document_host_verification_changed')
|
|
51
|
+
return [{'type':'memberOf','expression':'ec2InstanceId in ['+', '.join("'"+value+"'"for value in sorted(instances))+']'}]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class HostApi:
|
|
55
|
+
def __init__(self,region,*,ecs=None,ssm=None):
|
|
56
|
+
if not isinstance(region,str)or not re.fullmatch('[a-z]{2}-[a-z]+-[1-9]',region):raise ValueError('Use the owned AWS region')
|
|
57
|
+
self.region=region;self.clients={'ecs':ecs,'ssm':ssm};self._lock=threading.Lock()
|
|
58
|
+
def call(self,service,operation,**values):
|
|
59
|
+
allowed={'ecs':{'list_container_instances','describe_container_instances'},'ssm':{'get_document','send_command','get_command_invocation'}}
|
|
60
|
+
if service not in allowed or operation not in allowed[service]:raise ValueError('Use the fixed host-inspection contract')
|
|
61
|
+
try:
|
|
62
|
+
if self.clients[service]is None:
|
|
63
|
+
with self._lock:
|
|
64
|
+
if self.clients[service]is None:
|
|
65
|
+
import boto3
|
|
66
|
+
from botocore.config import Config
|
|
67
|
+
self.clients[service]=boto3.Session().client(service,region_name=self.region,
|
|
68
|
+
endpoint_url='https://'+service+'.'+self.region+'.amazonaws.com',
|
|
69
|
+
config=Config(proxies={},connect_timeout=2,read_timeout=5,retries={'total_max_attempts':1}))
|
|
70
|
+
value=getattr(self.clients[service],operation)(**values)
|
|
71
|
+
if not isinstance(value,dict):raise ValueError()
|
|
72
|
+
return value
|
|
73
|
+
except Exception as error:
|
|
74
|
+
if service=='ssm'and operation=='get_command_invocation'and getattr(error,'response',{}).get('Error',{}).get('Code')=='InvocationDoesNotExist':
|
|
75
|
+
return {'Status':'Pending'}
|
|
76
|
+
raise SourceUnavailable('document_host_verification_unavailable')from None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class EcsHostPolicy:
|
|
80
|
+
def __init__(self,config,*,api=None,poll_seconds=1):
|
|
81
|
+
from enterprise_aws_jobs import configuration
|
|
82
|
+
self.config=MappingProxyType(configuration(config));self.api=api or HostApi(self.config['region'])
|
|
83
|
+
if type(poll_seconds)not in {int,float}or not .01<=poll_seconds<=2:raise ValueError('Use a bounded host-inspection poll interval')
|
|
84
|
+
self.poll_seconds=poll_seconds
|
|
85
|
+
def __call__(self,config):
|
|
86
|
+
if config!=self.config:raise ValueError('Use the exact owned host policy destination')
|
|
87
|
+
try:return self.verify()
|
|
88
|
+
except Exception:raise SourceUnavailable('document_host_verification_unavailable')from None
|
|
89
|
+
def verify(self):
|
|
90
|
+
c=self.config;started=time.monotonic();cluster=c['clusterArn']
|
|
91
|
+
def call(service,operation,**values):
|
|
92
|
+
if time.monotonic()-started>60:raise ValueError('Host inspection deadline')
|
|
93
|
+
return self.api.call(service,operation,**values)
|
|
94
|
+
listing=call('ecs','list_container_instances',cluster=cluster,status='ACTIVE',maxResults=10,
|
|
95
|
+
filter='attribute:atmem-document-policy == '+c['hostPolicySha256'])
|
|
96
|
+
arns=listing.get('containerInstanceArns')
|
|
97
|
+
if not isinstance(arns,list)or not 1<=len(arns)<=10 or len(set(arns))!=len(arns)or listing.get('nextToken'):raise ValueError('Owned host pool unavailable')
|
|
98
|
+
observed=call('ecs','describe_container_instances',cluster=cluster,containerInstances=arns)
|
|
99
|
+
instances=observed.get('containerInstances')
|
|
100
|
+
if observed.get('failures')or not isinstance(instances,list)or len(instances)!=len(arns):raise ValueError('Owned host pool changed')
|
|
101
|
+
members=[]
|
|
102
|
+
for value in instances:
|
|
103
|
+
attributes=value.get('attributes',[])
|
|
104
|
+
if (value.get('containerInstanceArn')not in arns or value.get('agentConnected')is not True or value.get('status')!='ACTIVE'or
|
|
105
|
+
not isinstance(attributes,list)or sum(v.get('name')=='atmem-document-policy'and v.get('value')==c['hostPolicySha256']for v in attributes)!=1):
|
|
106
|
+
raise ValueError('Native host selection changed')
|
|
107
|
+
members.append((value['containerInstanceArn'],value.get('ec2InstanceId')))
|
|
108
|
+
verified=VerifiedHosts(c['hostPolicySha256'],tuple(members),time.monotonic()+120);verified.placement(c)
|
|
109
|
+
version=c['hostDocumentVersion'];document=call('ssm','get_document',Name=c['hostDocumentArn'],DocumentVersion=version,DocumentFormat='JSON')
|
|
110
|
+
content=document.get('Content')
|
|
111
|
+
if not isinstance(content,str)or len(content.encode())>65536 or document.get('DocumentVersion')!=version or json.loads(content,object_pairs_hook=unique)!=command_document(c):
|
|
112
|
+
raise ValueError('Owned command document content changed')
|
|
113
|
+
sent=call('ssm','send_command',InstanceIds=[instance for _,instance in members],DocumentName=c['hostDocumentArn'],
|
|
114
|
+
DocumentVersion=version,DocumentHash=c['hostDocumentHash'],DocumentHashType='Sha256',TimeoutSeconds=30,
|
|
115
|
+
CloudWatchOutputConfig={'CloudWatchOutputEnabled':False})
|
|
116
|
+
command=sent.get('Command',{}).get('CommandId')
|
|
117
|
+
if not isinstance(command,str)or not re.fullmatch('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',command):raise ValueError('Invalid native inspection identity')
|
|
118
|
+
for _,instance in members:
|
|
119
|
+
while True:
|
|
120
|
+
value=call('ssm','get_command_invocation',CommandId=command,InstanceId=instance,PluginName='verifyHost')
|
|
121
|
+
status=value.get('Status')
|
|
122
|
+
if status=='Success':break
|
|
123
|
+
if status not in {'Pending','InProgress','Delayed'}:raise ValueError('Host inspection failed')
|
|
124
|
+
time.sleep(self.poll_seconds)
|
|
125
|
+
if (value.get('CommandId')!=command or value.get('InstanceId')!=instance or value.get('DocumentVersion')!=version or
|
|
126
|
+
value.get('DocumentName')not in {c['hostDocumentArn'],c['hostDocumentArn'].rsplit('/',1)[1]}or
|
|
127
|
+
type(value.get('ResponseCode'))is not int or value['ResponseCode']!=0 or value.get('StandardErrorContent')or
|
|
128
|
+
value.get('StandardOutputUrl')or value.get('StandardErrorUrl')):raise ValueError('Host inspection identity changed')
|
|
129
|
+
encoded=value.get('StandardOutputContent')
|
|
130
|
+
if not isinstance(encoded,str)or not 0<len(encoded.encode())<=4096:raise ValueError('Invalid host observation')
|
|
131
|
+
output=json.loads(encoded,object_pairs_hook=unique)
|
|
132
|
+
if (not isinstance(output,dict)or set(output)!={'format','policySha256','clusterArn','bootId'}or
|
|
133
|
+
output['format']!='enterprise-aws-host-observation-v1'or output['policySha256']!=c['hostPolicySha256']or output['clusterArn']!=cluster or
|
|
134
|
+
not isinstance(output['bootId'],str)or not re.fullmatch('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',output['bootId'])):
|
|
135
|
+
raise ValueError('Host observation changed')
|
|
136
|
+
verified.placement(c);return verified
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Owned EC2/ECS per-file controller, not a complete production composition.
|
|
2
|
+
|
|
3
|
+
The parser requires per-container network disabling, unavailable on Fargate.
|
|
4
|
+
Host-policy verification/provisioning and complete export integration are required;
|
|
5
|
+
this module does not register a capability or change existing cloud resources.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import copy
|
|
9
|
+
import re
|
|
10
|
+
import hashlib
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import MappingProxyType
|
|
16
|
+
from enterprise_aws_objects import S3DocumentObjects
|
|
17
|
+
from enterprise_document_job import request,result
|
|
18
|
+
from enterprise_document_jobs import DocumentJobLedger
|
|
19
|
+
from enterprise_sources import SourceUnavailable
|
|
20
|
+
from enterprise_aws_hosts import EcsHostPolicy,VerifiedHosts
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def configuration(value):
|
|
24
|
+
fields={'format','accountId','region','bucket','clusterArn','hostPolicySha256','hostDocumentArn','hostDocumentHash','hostDocumentVersion','workerRoleArn',
|
|
25
|
+
'transferRoleArn','executionRoleArn','parserImage','transferImage','taskDefinitionArn'}
|
|
26
|
+
if not isinstance(value,dict)or set(value)!=fields or value['format']!='enterprise-aws-document-job-v2':
|
|
27
|
+
raise ValueError('Use the exact owned AWS document-job configuration')
|
|
28
|
+
patterns={'accountId':r'[0-9]{12}','region':r'(?:af|ap|ca|eu|il|me|mx|sa|us)-(?:east|west|south|north|central|southeast|southwest|northeast|northwest)-[1-9]',
|
|
29
|
+
'bucket':r'[a-z0-9][a-z0-9-]{1,61}[a-z0-9]'}
|
|
30
|
+
for key,pattern in patterns.items():
|
|
31
|
+
if not isinstance(value[key],str)or not re.fullmatch(pattern,value[key]):raise ValueError('Invalid owned AWS destination')
|
|
32
|
+
if not isinstance(value['hostPolicySha256'],str)or not re.fullmatch('[0-9a-f]{64}',value['hostPolicySha256']):
|
|
33
|
+
raise ValueError('Use the reviewed dedicated-host policy fingerprint')
|
|
34
|
+
account,region=value['accountId'],value['region']
|
|
35
|
+
prefix='arn:aws:'
|
|
36
|
+
if (not isinstance(value['hostDocumentArn'],str)or not re.fullmatch(re.escape(prefix+'ssm:'+region+':'+account+':document/')+r'[A-Za-z0-9_.-]{3,128}',value['hostDocumentArn'])or
|
|
37
|
+
not isinstance(value['hostDocumentHash'],str)or not re.fullmatch('[0-9a-f]{64}',value['hostDocumentHash'])or
|
|
38
|
+
not isinstance(value['hostDocumentVersion'],str)or not re.fullmatch('[1-9][0-9]{0,8}',value['hostDocumentVersion'])):
|
|
39
|
+
raise ValueError('Use the exact owned version/hash-bound host document')
|
|
40
|
+
for key in ['workerRoleArn','transferRoleArn','executionRoleArn']:
|
|
41
|
+
if not isinstance(value[key],str)or not re.fullmatch(re.escape(prefix+'iam::'+account+':role/')+r'[A-Za-z0-9_+=,.@/-]{1,128}',value[key]):
|
|
42
|
+
raise ValueError('Use explicit same-account owned roles')
|
|
43
|
+
if len({value[key]for key in ['workerRoleArn','transferRoleArn','executionRoleArn']})!=3:
|
|
44
|
+
raise ValueError('Worker, storage transfer and image execution authority must be distinct')
|
|
45
|
+
if not isinstance(value['clusterArn'],str)or not re.fullmatch(re.escape(prefix+'ecs:'+region+':'+account+':cluster/')+r'[A-Za-z0-9_-]{1,255}',value['clusterArn']):
|
|
46
|
+
raise ValueError('Use the owned regional ECS cluster')
|
|
47
|
+
if (not isinstance(value['taskDefinitionArn'],str)or not re.fullmatch(re.escape(prefix+'ecs:'+region+':'+account+':task-definition/document-')+r'[A-Za-z0-9_-]{1,200}:[1-9][0-9]{0,8}',value['taskDefinitionArn'])):
|
|
48
|
+
raise ValueError('Use the exact operator-owned regional task-definition revision')
|
|
49
|
+
for key in ['parserImage','transferImage']:
|
|
50
|
+
if not isinstance(value[key],str)or not re.fullmatch(re.escape(account+'.dkr.ecr.'+region+'.amazonaws.com/')+r'[a-z0-9]+(?:[._/-][a-z0-9]+)*@sha256:[0-9a-f]{64}',value[key]):
|
|
51
|
+
raise ValueError('Use immutable owned regional ECR images')
|
|
52
|
+
if value['parserImage']==value['transferImage']:raise ValueError('Keep the parser and trusted transfer artifacts separate')
|
|
53
|
+
return dict(value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def transfer_metadata(config,metadata,etag):
|
|
57
|
+
# Preserve the shared byte contract's exact fields and canonical media type.
|
|
58
|
+
if not isinstance(metadata,dict)or set(metadata)!={'format','requestId','sha256','mediaType'}:
|
|
59
|
+
raise ValueError('Use the exact document request')
|
|
60
|
+
if (metadata['format']!='enterprise-document-job-v1' or
|
|
61
|
+
not isinstance(metadata['requestId'],str)or not re.fullmatch('[0-9a-f]{64}',metadata['requestId'])or
|
|
62
|
+
not isinstance(metadata['sha256'],str)or not re.fullmatch('[0-9a-f]{64}',metadata['sha256'])):
|
|
63
|
+
raise ValueError('Use the owned byte-bound request')
|
|
64
|
+
from enterprise_document_contract import media_type
|
|
65
|
+
if media_type(metadata['mediaType'])!=metadata['mediaType']:raise ValueError('Use canonical document MIME')
|
|
66
|
+
# Opaque ETag is a conditional read token, never treated as an input digest.
|
|
67
|
+
if not isinstance(etag,str)or not re.fullmatch(r'"[0-9a-f]{32}(?:-[1-9][0-9]{0,4})?"',etag):
|
|
68
|
+
raise ValueError('Use the acknowledged native input ETag')
|
|
69
|
+
return {'format':'enterprise-aws-document-transfer-v1','accountId':config['accountId'],
|
|
70
|
+
'region':config['region'],'bucket':config['bucket'],'request':dict(metadata),'inputETag':etag}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def task_family(config):
|
|
74
|
+
return config['taskDefinitionArn'].rsplit('/',1)[1].rsplit(':',1)[0]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def task_definition(config):
|
|
78
|
+
"""Immutable operator-owned shape, shared with the Terraform compiler.
|
|
79
|
+
|
|
80
|
+
No file identity, bytes, ETag, user prompt or model authority belongs here.
|
|
81
|
+
"""
|
|
82
|
+
owned=configuration(config)
|
|
83
|
+
replacements={'__FAMILY__':task_family(owned),'__POLICY_SHA256__':owned['hostPolicySha256'],
|
|
84
|
+
'__TRANSFER_ROLE_ARN__':owned['transferRoleArn'],'__EXECUTION_ROLE_ARN__':owned['executionRoleArn'],
|
|
85
|
+
'__TRANSFER_IMAGE__':owned['transferImage'],'__PARSER_IMAGE__':owned['parserImage']}
|
|
86
|
+
def resolve(value):
|
|
87
|
+
if isinstance(value,dict):return {key:resolve(item)for key,item in value.items()}
|
|
88
|
+
if isinstance(value,list):return [resolve(item)for item in value]
|
|
89
|
+
if isinstance(value,str):
|
|
90
|
+
for key,replacement in replacements.items():value=value.replace(key,replacement)
|
|
91
|
+
return value
|
|
92
|
+
return resolve(json.loads(Path(__file__).with_name('enterprise-aws-task-template.json').read_text()))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def task_overrides(config,metadata,etag):
|
|
96
|
+
encoded=json.dumps(transfer_metadata(configuration(config),metadata,etag),separators=(',',':'))
|
|
97
|
+
return {'containerOverrides':[{'name':name,'environment':[{'name':'DOCUMENT_JOB','value':encoded}]}for name in ['download','upload']]}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def verify_definition(config,reply):
|
|
101
|
+
"""Reject drift before storing bytes; tolerate only inert native defaults.
|
|
102
|
+
|
|
103
|
+
Native describe metadata is not a runtime override or deployment proof.
|
|
104
|
+
Unknown nonempty configuration fails closed rather than being projected away.
|
|
105
|
+
"""
|
|
106
|
+
desired=task_definition(config);desired.pop('tags')
|
|
107
|
+
if not isinstance(reply,dict)or not isinstance(reply.get('taskDefinition'),dict):raise SourceUnavailable('document_definition_unavailable')
|
|
108
|
+
actual=copy.deepcopy(reply['taskDefinition'])
|
|
109
|
+
if (actual.pop('taskDefinitionArn',None)!=config['taskDefinitionArn']or
|
|
110
|
+
type(actual.get('revision'))is not int or actual.pop('revision')!=int(config['taskDefinitionArn'].rsplit(':',1)[1])or
|
|
111
|
+
actual.pop('status',None)!='ACTIVE'):
|
|
112
|
+
raise SourceUnavailable('document_definition_unavailable')
|
|
113
|
+
for field in ['registeredAt','registeredBy','compatibilities','requiresAttributes']:
|
|
114
|
+
actual.pop(field,None)
|
|
115
|
+
if actual.get('enableFaultInjection')is False:actual.pop('enableFaultInjection')
|
|
116
|
+
if not isinstance(actual.get('containerDefinitions'),list):raise SourceUnavailable('document_definition_unavailable')
|
|
117
|
+
for container in actual['containerDefinitions']:
|
|
118
|
+
if not isinstance(container,dict):raise SourceUnavailable('document_definition_unavailable')
|
|
119
|
+
if container.get('disableNetworking')is False:container.pop('disableNetworking')
|
|
120
|
+
if container.get('versionConsistency')=='enabled':container.pop('versionConsistency')
|
|
121
|
+
def canonical(value):
|
|
122
|
+
if isinstance(value,dict):return {key:canonical(item)for key,item in value.items()if item!=[]and item!={}}
|
|
123
|
+
if isinstance(value,list):return [canonical(item)for item in value]
|
|
124
|
+
return value
|
|
125
|
+
if json.dumps(canonical(actual),sort_keys=True)!=json.dumps(canonical(desired),sort_keys=True):raise SourceUnavailable('document_definition_unavailable')
|
|
126
|
+
return config['taskDefinitionArn']
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class EcsDocumentApi:
|
|
130
|
+
def __init__(self,region,*,client=None):
|
|
131
|
+
if not isinstance(region,str)or not re.fullmatch(r'[a-z]{2}-[a-z]+-[1-9]',region):raise ValueError('Use a reviewed commercial AWS region')
|
|
132
|
+
self.region,self.client=region,client;self._lock=threading.Lock()
|
|
133
|
+
|
|
134
|
+
def call(self,operation,**values):
|
|
135
|
+
allowed={'describe_task_definition','run_task','describe_tasks','list_tasks','stop_task'}
|
|
136
|
+
if operation not in allowed:raise ValueError('Use the owned document lifecycle')
|
|
137
|
+
try:
|
|
138
|
+
if self.client is None:
|
|
139
|
+
with self._lock:
|
|
140
|
+
if self.client is None:
|
|
141
|
+
import boto3
|
|
142
|
+
from botocore.config import Config
|
|
143
|
+
self.client=boto3.Session().client('ecs',region_name=self.region,
|
|
144
|
+
endpoint_url='https://ecs.'+self.region+'.amazonaws.com',
|
|
145
|
+
config=Config(proxies={},connect_timeout=2,read_timeout=8,retries={'total_max_attempts':1}))
|
|
146
|
+
value=getattr(self.client,operation)(**values)
|
|
147
|
+
if not isinstance(value,dict):raise ValueError()
|
|
148
|
+
return value
|
|
149
|
+
except Exception:raise SourceUnavailable('document_job_unavailable')from None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class EcsDocuments:
|
|
153
|
+
def __init__(self,config,*,host_check=None,api=None,objects=None,ledger=None,deadline=140,poll_seconds=1):
|
|
154
|
+
self.config=MappingProxyType(configuration(config))
|
|
155
|
+
if host_check is None:host_check=EcsHostPolicy(dict(self.config))
|
|
156
|
+
if not callable(host_check):raise ValueError('Supply the owned dedicated-host verifier')
|
|
157
|
+
if type(deadline)is not int or not 1<=deadline<=140 or type(poll_seconds)not in {int,float}or not .01<=poll_seconds<=2:
|
|
158
|
+
raise ValueError('Use bounded document deadlines')
|
|
159
|
+
self.host_check=host_check;self.api=api or EcsDocumentApi(self.config['region'])
|
|
160
|
+
self.objects=objects or S3DocumentObjects(self.config['bucket'],self.config['region'],self.config['accountId'])
|
|
161
|
+
self.ledger=ledger;self.deadline,self.poll_seconds=deadline,poll_seconds
|
|
162
|
+
self.cluster=self.config['clusterArn'];self.prefix='arn:aws:ecs:'+self.config['region']+':'+self.config['accountId']+':'
|
|
163
|
+
self.target=hashlib.sha256(json.dumps({**{key:self.config[key]for key in ['accountId','region','bucket','clusterArn']},'taskFamily':task_family(self.config)},sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
|
164
|
+
|
|
165
|
+
def __call__(self,raw,mime):raise SourceUnavailable('source_validation_required')
|
|
166
|
+
|
|
167
|
+
def bind_runtime(self,provider,environment):
|
|
168
|
+
if (provider!='aws'or environment.get('REGION')!=self.config['region']or
|
|
169
|
+
environment.get('AWS_ACCOUNT_ID')!=self.config['accountId']or environment.get('WORKER_ROLE_ARN')!=self.config['workerRoleArn']):
|
|
170
|
+
raise ValueError('Document execution must match the selected AWS destination and worker identity')
|
|
171
|
+
|
|
172
|
+
def attach_runtime(self,auth,account_id):
|
|
173
|
+
if self.ledger is None:self.ledger=DocumentJobLedger(auth,account_id,self.target)
|
|
174
|
+
elif (not isinstance(self.ledger,DocumentJobLedger)or self.ledger.auth is not auth or self.ledger.account_id!=account_id or self.ledger.target!=self.target):
|
|
175
|
+
raise ValueError('Use the owned persistent cleanup ledger')
|
|
176
|
+
|
|
177
|
+
def definition(self,arn):
|
|
178
|
+
if not isinstance(arn,str)or not re.fullmatch(re.escape(self.prefix+'task-definition/'+task_family(self.config)+':')+'[1-9][0-9]{0,8}',arn):
|
|
179
|
+
raise SourceUnavailable('document_job_unavailable')
|
|
180
|
+
return arn
|
|
181
|
+
|
|
182
|
+
def task(self,identifier,value,definition=None):
|
|
183
|
+
arn=value.get('taskArn');cluster_name=self.cluster.rsplit('/',1)[1]
|
|
184
|
+
if (not isinstance(arn,str)or not re.fullmatch(re.escape(self.prefix+'task/'+cluster_name+'/')+'[0-9a-f]{32}',arn)or
|
|
185
|
+
value.get('clusterArn')!=self.cluster or value.get('startedBy')!=identifier):raise SourceUnavailable('document_job_unavailable')
|
|
186
|
+
owned=self.definition(value.get('taskDefinitionArn'))
|
|
187
|
+
if definition is not None and owned!=definition:raise SourceUnavailable('document_job_unavailable')
|
|
188
|
+
return arn
|
|
189
|
+
|
|
190
|
+
def from_source(self,raw,mime,guard):
|
|
191
|
+
if not callable(guard):raise ValueError('Supply the live source/company/job check')
|
|
192
|
+
identifier=uuid.uuid4().hex+uuid.uuid4().hex;metadata=request(raw,mime,identifier)
|
|
193
|
+
guard()
|
|
194
|
+
if self.ledger is None:raise SourceUnavailable('document_job_recovery_required')
|
|
195
|
+
hosts=self.host_check(dict(self.config));guard()
|
|
196
|
+
if not isinstance(hosts,VerifiedHosts):raise SourceUnavailable('document_host_verification_required')
|
|
197
|
+
hosts.placement(dict(self.config))
|
|
198
|
+
definition=verify_definition(dict(self.config),self.api.call('describe_task_definition',taskDefinition=self.config['taskDefinitionArn']))
|
|
199
|
+
guard()
|
|
200
|
+
lease=self.ledger.allocate(identifier);known=None;started=time.monotonic()
|
|
201
|
+
def check():self.ledger.guard(lease);guard();self.ledger.guard(lease)
|
|
202
|
+
try:
|
|
203
|
+
check();condition=self.objects.put(identifier,'input',raw)
|
|
204
|
+
self.ledger.advance(lease,'job_pending');check()
|
|
205
|
+
launched=self.api.call('run_task',cluster=self.cluster,taskDefinition=definition,launchType='EC2',count=1,
|
|
206
|
+
clientToken=identifier,startedBy=identifier,enableExecuteCommand=False,enableECSManagedTags=False,
|
|
207
|
+
overrides=task_overrides(dict(self.config),metadata,condition),
|
|
208
|
+
placementConstraints=hosts.placement(dict(self.config)))
|
|
209
|
+
if launched.get('failures')or len(launched.get('tasks',[]))!=1:raise SourceUnavailable('document_job_unavailable')
|
|
210
|
+
known=self.task(identifier,launched['tasks'][0],definition);self.ledger.advance(lease,'job_running')
|
|
211
|
+
while True:
|
|
212
|
+
check()
|
|
213
|
+
if time.monotonic()-started>self.deadline:raise SourceUnavailable('document_job_unavailable')
|
|
214
|
+
observed=self.api.call('describe_tasks',cluster=self.cluster,tasks=[known])
|
|
215
|
+
if observed.get('failures')or len(observed.get('tasks',[]))!=1:raise SourceUnavailable('document_job_unavailable')
|
|
216
|
+
task=observed['tasks'][0]
|
|
217
|
+
if self.task(identifier,task,definition)!=known:raise SourceUnavailable('document_job_unavailable')
|
|
218
|
+
if task.get('lastStatus')=='STOPPED':
|
|
219
|
+
if task.get('containerInstanceArn')not in {arn for arn,_ in hosts.members}:raise SourceUnavailable('document_host_verification_changed')
|
|
220
|
+
containers=task.get('containers',[])
|
|
221
|
+
if (len(containers)!=3 or {v.get('name')for v in containers}!={'download','parser','upload'}or
|
|
222
|
+
any(v.get('lastStatus')!='STOPPED'or type(v.get('exitCode'))is not int or v['exitCode']!=0 for v in containers)):
|
|
223
|
+
raise SourceUnavailable('document_job_unavailable')
|
|
224
|
+
break
|
|
225
|
+
if task.get('lastStatus')not in {'PROVISIONING','PENDING','ACTIVATING','RUNNING','DEACTIVATING','STOPPING','DEPROVISIONING'}:
|
|
226
|
+
raise SourceUnavailable('document_job_unavailable')
|
|
227
|
+
time.sleep(self.poll_seconds)
|
|
228
|
+
self.ledger.advance(lease,'succeeded');check()
|
|
229
|
+
value=result(raw,mime,identifier,self.objects.load(identifier,'result'));check();return value
|
|
230
|
+
finally:
|
|
231
|
+
cleanup=self.ledger.claim_owner(lease)
|
|
232
|
+
try:self.cleanup(identifier,known=known)
|
|
233
|
+
except BaseException:self.ledger.failed(cleanup);raise
|
|
234
|
+
else:self.ledger.completed(cleanup)
|
|
235
|
+
|
|
236
|
+
def listed(self,operation,key,*,_deadline=None,**values):
|
|
237
|
+
found=[];token=None;seen=set()
|
|
238
|
+
for _ in range(5):
|
|
239
|
+
if _deadline is not None and time.monotonic()>_deadline:raise SourceUnavailable('document_cleanup_unavailable')
|
|
240
|
+
reply=self.api.call(operation,**values,**({'nextToken':token}if token else{}))
|
|
241
|
+
items=reply.get(key)
|
|
242
|
+
if not isinstance(items,list)or any(not isinstance(v,str)for v in items):raise SourceUnavailable('document_cleanup_unavailable')
|
|
243
|
+
found.extend(items);token=reply.get('nextToken')
|
|
244
|
+
if token is None:return list(dict.fromkeys(found))
|
|
245
|
+
if not isinstance(token,str)or not 0<len(token)<=4096 or token in seen:break
|
|
246
|
+
seen.add(token)
|
|
247
|
+
raise SourceUnavailable('document_cleanup_unavailable')
|
|
248
|
+
|
|
249
|
+
def cleanup(self,identifier,*,known=None):
|
|
250
|
+
if not isinstance(identifier,str)or not re.fullmatch('[0-9a-f]{64}',identifier):raise ValueError('Use the owned cleanup identity')
|
|
251
|
+
tasks=[];failure=False;deadline=time.monotonic()+70
|
|
252
|
+
def call(operation,**values):
|
|
253
|
+
if time.monotonic()>deadline:raise SourceUnavailable('document_cleanup_unavailable')
|
|
254
|
+
return self.api.call(operation,**values)
|
|
255
|
+
try:
|
|
256
|
+
# AWS startedBy is the only filter permitted alongside cluster.
|
|
257
|
+
# Default RUNNING desired status also includes pending launches.
|
|
258
|
+
# Unknown terminal tasks are left to native task-volume cleanup;
|
|
259
|
+
# uncertain creation keeps its durable settlement/retry window.
|
|
260
|
+
tasks+=self.listed('list_tasks','taskArns',_deadline=deadline,cluster=self.cluster,startedBy=identifier,maxResults=100)
|
|
261
|
+
if known is not None:tasks.append(known)
|
|
262
|
+
tasks=list(dict.fromkeys(tasks))
|
|
263
|
+
if len(tasks)>10:raise SourceUnavailable('document_cleanup_unavailable')
|
|
264
|
+
for arn in tasks:
|
|
265
|
+
observed=call('describe_tasks',cluster=self.cluster,tasks=[arn])
|
|
266
|
+
if observed.get('failures')or len(observed.get('tasks',[]))!=1:raise SourceUnavailable('document_cleanup_unavailable')
|
|
267
|
+
value=observed['tasks'][0]
|
|
268
|
+
if self.task(identifier,value)!=arn:raise SourceUnavailable('document_cleanup_unavailable')
|
|
269
|
+
if value.get('lastStatus')!='STOPPED':
|
|
270
|
+
call('stop_task',cluster=self.cluster,task=arn,reason='Owned document cleanup')
|
|
271
|
+
started=time.monotonic()
|
|
272
|
+
while True:
|
|
273
|
+
reply=call('describe_tasks',cluster=self.cluster,tasks=[arn])
|
|
274
|
+
if reply.get('failures')or len(reply.get('tasks',[]))!=1:raise SourceUnavailable('document_cleanup_unavailable')
|
|
275
|
+
current=reply['tasks'][0]
|
|
276
|
+
if self.task(identifier,current)!=arn:raise SourceUnavailable('document_cleanup_unavailable')
|
|
277
|
+
if current.get('lastStatus')=='STOPPED':break
|
|
278
|
+
if time.monotonic()-started>20:raise SourceUnavailable('document_cleanup_unavailable')
|
|
279
|
+
time.sleep(self.poll_seconds)
|
|
280
|
+
except Exception:failure=True
|
|
281
|
+
# Even a native task cleanup failure still attempts both owned
|
|
282
|
+
# objects. Return no document text until the entire cleanup succeeds.
|
|
283
|
+
for side in ['input','result']:
|
|
284
|
+
try:self.objects.delete(identifier,side)
|
|
285
|
+
except Exception:failure=True
|
|
286
|
+
if failure:raise SourceUnavailable('document_cleanup_unavailable')
|
|
287
|
+
|
|
288
|
+
def reconcile(self,*,limit=5):
|
|
289
|
+
if self.ledger is None:raise SourceUnavailable('document_job_recovery_required')
|
|
290
|
+
if type(limit)is not int or not 1<=limit<=20:raise ValueError('Use bounded cleanup batches')
|
|
291
|
+
counts={'claimed':0,'retired':0,'settling':0,'failed':0}
|
|
292
|
+
for _ in range(limit):
|
|
293
|
+
lease=self.ledger.claim_due()
|
|
294
|
+
if lease is None:break
|
|
295
|
+
counts['claimed']+=1
|
|
296
|
+
try:self.cleanup(lease.request_id)
|
|
297
|
+
except Exception:self.ledger.failed(lease);counts['failed']+=1
|
|
298
|
+
else:
|
|
299
|
+
retired=self.ledger.completed(lease);counts['retired'if retired else'settling']+=1
|
|
300
|
+
return counts
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Bounded, owner-checked temporary S3 document bytes, not a source connector."""
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
import threading
|
|
6
|
+
from enterprise_document_contract import MAX_INPUT
|
|
7
|
+
from enterprise_document_job import MAX_ENVELOPE
|
|
8
|
+
from enterprise_sources import SourceUnavailable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def object_key(identifier,side):
|
|
12
|
+
if not isinstance(identifier,str)or not re.fullmatch('[0-9a-f]{64}',identifier)or side not in {'input','result'}:
|
|
13
|
+
raise ValueError('Use the owned per-file object identity')
|
|
14
|
+
return 'document-jobs/'+identifier+'/'+side
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def etag(value):
|
|
18
|
+
if not isinstance(value,str)or not re.fullmatch(r'"[0-9a-f]{32}(?:-[1-9][0-9]{0,4})?"',value):
|
|
19
|
+
raise SourceUnavailable('document_object_unavailable')
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class S3DocumentObjects:
|
|
24
|
+
def __init__(self,bucket,region,account,*,client=None):
|
|
25
|
+
if (not isinstance(bucket,str)or not re.fullmatch('[a-z0-9][a-z0-9-]{1,61}[a-z0-9]',bucket)or
|
|
26
|
+
not isinstance(region,str)or not re.fullmatch(r'(?:af|ap|ca|eu|il|me|mx|sa|us)-(?:east|west|south|north|central|southeast|southwest|northeast|northwest)-[1-9]',region)or
|
|
27
|
+
not isinstance(account,str)or not re.fullmatch('[0-9]{12}',account)):
|
|
28
|
+
raise ValueError('Use an owned regional temporary bucket')
|
|
29
|
+
self.bucket,self.region,self.account=bucket,region,account
|
|
30
|
+
self.client=client;self._lock=threading.Lock()
|
|
31
|
+
|
|
32
|
+
def _client(self):
|
|
33
|
+
if self.client is None:
|
|
34
|
+
with self._lock:
|
|
35
|
+
if self.client is None:
|
|
36
|
+
import boto3
|
|
37
|
+
from botocore.config import Config
|
|
38
|
+
self.client=boto3.Session().client('s3',region_name=self.region,
|
|
39
|
+
endpoint_url='https://s3.'+self.region+'.amazonaws.com',
|
|
40
|
+
config=Config(proxies={},connect_timeout=2,read_timeout=8,retries={'total_max_attempts':1},
|
|
41
|
+
s3={'addressing_style':'virtual'}))
|
|
42
|
+
return self.client
|
|
43
|
+
|
|
44
|
+
def owner(self):return {'Bucket':self.bucket,'ExpectedBucketOwner':self.account}
|
|
45
|
+
|
|
46
|
+
def preflight(self):
|
|
47
|
+
"""No payload mutation if native placement or no-history policy changed."""
|
|
48
|
+
try:
|
|
49
|
+
client=self._client();where=client.get_bucket_location(**self.owner()).get('LocationConstraint')
|
|
50
|
+
region='us-east-1'if where is None else'eu-west-1'if where=='EU'else where
|
|
51
|
+
history=client.get_bucket_versioning(**self.owner())
|
|
52
|
+
if region!=self.region or history.get('Status')is not None or history.get('MFADelete')is not None:raise ValueError()
|
|
53
|
+
except Exception:raise SourceUnavailable('document_object_policy_unavailable')from None
|
|
54
|
+
|
|
55
|
+
def put(self,identifier,side,raw):
|
|
56
|
+
key=object_key(identifier,side);maximum=MAX_INPUT if side=='input'else MAX_ENVELOPE
|
|
57
|
+
if not isinstance(raw,bytes)or not 0<len(raw)<=maximum:raise ValueError('Use bounded owned document bytes')
|
|
58
|
+
self.preflight()
|
|
59
|
+
try:
|
|
60
|
+
value=self._client().put_object(**self.owner(),Key=key,Body=raw,IfNoneMatch='*',
|
|
61
|
+
ContentType='application/octet-stream',ServerSideEncryption='AES256',
|
|
62
|
+
ChecksumSHA256=base64.b64encode(hashlib.sha256(raw).digest()).decode())
|
|
63
|
+
if value.get('VersionId')not in {None,'null'}or value.get('ServerSideEncryption')!='AES256':raise ValueError()
|
|
64
|
+
return etag(value.get('ETag'))
|
|
65
|
+
except Exception:raise SourceUnavailable('document_object_unavailable')from None
|
|
66
|
+
|
|
67
|
+
def load(self,identifier,side,condition=None):
|
|
68
|
+
key=object_key(identifier,side);maximum=MAX_INPUT if side=='input'else MAX_ENVELOPE
|
|
69
|
+
extra={}if condition is None else {'IfMatch':etag(condition)}
|
|
70
|
+
self.preflight()
|
|
71
|
+
stream=None
|
|
72
|
+
try:
|
|
73
|
+
value=self._client().get_object(**self.owner(),Key=key,**extra);stream=value['Body']
|
|
74
|
+
if (value.get('VersionId')not in {None,'null'}or value.get('ServerSideEncryption')!='AES256'or
|
|
75
|
+
type(value.get('ContentLength'))is not int or not 0<value['ContentLength']<=maximum or
|
|
76
|
+
(condition is not None and value.get('ETag')!=condition)):raise ValueError()
|
|
77
|
+
raw=stream.read(maximum+1)
|
|
78
|
+
if not isinstance(raw,bytes)or len(raw)!=value['ContentLength']or not 0<len(raw)<=maximum:raise ValueError()
|
|
79
|
+
return raw
|
|
80
|
+
except Exception:raise SourceUnavailable('document_object_unavailable')from None
|
|
81
|
+
finally:
|
|
82
|
+
if stream is not None:
|
|
83
|
+
try:stream.close()
|
|
84
|
+
except Exception:raise SourceUnavailable('document_object_unavailable')from None
|
|
85
|
+
|
|
86
|
+
def delete(self,identifier,side):
|
|
87
|
+
key=object_key(identifier,side)
|
|
88
|
+
# Never treat a delete marker as erasure of retained object versions.
|
|
89
|
+
self.preflight()
|
|
90
|
+
try:
|
|
91
|
+
value=self._client().delete_object(**self.owner(),Key=key)
|
|
92
|
+
if value.get('DeleteMarker')or value.get('VersionId')not in {None,'null'}:raise ValueError()
|
|
93
|
+
except Exception:raise SourceUnavailable('document_cleanup_unavailable')from None
|