@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,161 @@
|
|
|
1
|
+
"""Owned Batch pool observation before source upload; never provisions a pool.
|
|
2
|
+
|
|
3
|
+
The contract is separate from the fourteen-field job/host policy to avoid a
|
|
4
|
+
bootstrap/hash cycle. Native installation, IAM/network resources and actual pool
|
|
5
|
+
image compatibility still need infrastructure/artifact and account evidence.
|
|
6
|
+
"""
|
|
7
|
+
import copy
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from enterprise_azure_jobs import configuration as job_configuration, fingerprint, typed_equal
|
|
12
|
+
from enterprise_sources import SourceUnavailable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sha(value):
|
|
16
|
+
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()).hexdigest()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def configuration(value, job):
|
|
20
|
+
job = job_configuration(job)
|
|
21
|
+
fields = {'format', 'identityResourceId', 'subnetId', 'vmSize', 'imageReference',
|
|
22
|
+
'nodeAgentSkuId', 'startTaskSha256', 'dedicatedNodes', 'taskSlotsPerNode'}
|
|
23
|
+
if not isinstance(value, dict) or set(value) != fields or value['format'] != 'enterprise-azure-document-pool-v1':
|
|
24
|
+
raise ValueError('Use the exact owned Azure pool contract')
|
|
25
|
+
base = re.escape('/subscriptions/' + job['subscriptionId'] + '/resourceGroups/')
|
|
26
|
+
group = r'[A-Za-z0-9_().-]{1,90}'
|
|
27
|
+
name = r'[A-Za-z0-9_-]{1,80}'
|
|
28
|
+
patterns = {'identityResourceId': base + group + r'/providers/Microsoft.ManagedIdentity/userAssignedIdentities/' + name,
|
|
29
|
+
'subnetId': base + group + r'/providers/Microsoft.Network/virtualNetworks/' + name + '/subnets/' + name,
|
|
30
|
+
'vmSize': r'Standard_[A-Za-z0-9_]{2,64}', 'startTaskSha256': r'[0-9a-f]{64}',
|
|
31
|
+
'nodeAgentSkuId': r'batch\.node\.[a-z][a-z0-9 ._-]{1,64}'}
|
|
32
|
+
for key, pattern in patterns.items():
|
|
33
|
+
if not isinstance(value[key], str) or not re.fullmatch(pattern, value[key]):
|
|
34
|
+
raise ValueError('Invalid owned Azure pool placement or artifact contract')
|
|
35
|
+
image = value['imageReference']
|
|
36
|
+
if (not isinstance(image, dict) or set(image) != {'publisher', 'offer', 'sku', 'version'} or
|
|
37
|
+
not isinstance(image['publisher'], str) or not isinstance(image['offer'], str) or
|
|
38
|
+
(image['publisher'], image['offer']) not in {('microsoft-dsvm', 'ubuntu-hpc'), ('almalinux', '8-hpc-gen1'), ('almalinux', '8-hpc-gen2')} or
|
|
39
|
+
not isinstance(image['sku'], str) or not re.fullmatch('[a-zA-Z0-9_.-]{1,64}', image['sku']) or
|
|
40
|
+
not isinstance(image['version'], str) or not re.fullmatch(r'[0-9]{1,12}\.[0-9]{1,12}\.[0-9]{1,12}', image['version'])):
|
|
41
|
+
raise ValueError('Use a pinned reviewed container-compatible Linux image')
|
|
42
|
+
if (type(value['dedicatedNodes']) is not int or not 1 <= value['dedicatedNodes'] <= 100 or
|
|
43
|
+
type(value['taskSlotsPerNode']) is not int or not 1 <= value['taskSlotsPerNode'] <= 4):
|
|
44
|
+
raise ValueError('Use explicit bounded dedicated pool capacity')
|
|
45
|
+
return copy.deepcopy(value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def metadata(value):
|
|
49
|
+
if not isinstance(value, list): raise ValueError('Native ownership metadata is required')
|
|
50
|
+
result = {}
|
|
51
|
+
for item in value:
|
|
52
|
+
if (not isinstance(item, dict) or set(item) != {'name', 'value'} or not isinstance(item['name'], str) or
|
|
53
|
+
not isinstance(item['value'], str) or item['name'] in result):
|
|
54
|
+
raise ValueError('Ambiguous native pool ownership metadata')
|
|
55
|
+
result[item['name']] = item['value']
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def image_matches(expected, observed):
|
|
60
|
+
if not isinstance(observed, dict): return False
|
|
61
|
+
allowed = set(expected) | {'exactVersion'}
|
|
62
|
+
return (not set(observed) - allowed and all(observed.get(k) == v for k, v in expected.items()) and
|
|
63
|
+
observed.get('exactVersion', expected['version']) == expected['version'])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def verify_start_task(value, contract, job):
|
|
67
|
+
fields = {'commandLine', 'resourceFiles', 'userIdentity', 'maxTaskRetryCount', 'waitForSuccess'}
|
|
68
|
+
if (not isinstance(value, dict) or set(value) != fields or value['waitForSuccess'] is not True or
|
|
69
|
+
type(value['maxTaskRetryCount']) is not int or value['maxTaskRetryCount'] != 0 or
|
|
70
|
+
value['userIdentity'] != {'autoUser': {'scope': 'pool', 'elevationLevel': 'admin'}} or
|
|
71
|
+
not isinstance(value['commandLine'], str) or len(value['commandLine'].encode()) > 32768 or
|
|
72
|
+
not value['commandLine'].startswith('/usr/bin/python3 -I -c ') or
|
|
73
|
+
not isinstance(value['resourceFiles'], list) or not 1 <= len(value['resourceFiles']) <= 3 or
|
|
74
|
+
sha(value) != contract['startTaskSha256']):
|
|
75
|
+
raise ValueError('Owned native installation task changed')
|
|
76
|
+
for resource in value['resourceFiles']:
|
|
77
|
+
if (not isinstance(resource, dict) or set(resource) != {'httpUrl', 'filePath', 'fileMode', 'identityReference'} or
|
|
78
|
+
resource['identityReference'] != {'resourceId': contract['identityResourceId']} or
|
|
79
|
+
resource['fileMode'] != '0400' or not isinstance(resource['filePath'], str) or
|
|
80
|
+
not re.fullmatch(r'[a-z][a-z0-9-]{0,63}\.zip', resource['filePath']) or
|
|
81
|
+
not isinstance(resource['httpUrl'], str) or not re.fullmatch(
|
|
82
|
+
re.escape('https://' + job['storageAccount'] + '.blob.core.windows.net/bootstrap/') + r'[0-9a-f]{64}\.zip', resource['httpUrl'])):
|
|
83
|
+
raise ValueError('Use only immutable owned-MI bootstrap packages')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def verify_pool(job, contract, observed):
|
|
87
|
+
if (not isinstance(observed, dict) or observed.get('id') != job['poolId'] or observed.get('state') != 'active' or
|
|
88
|
+
observed.get('allocationState') != 'steady'):
|
|
89
|
+
raise ValueError('Owned native pool is not active')
|
|
90
|
+
if metadata(observed.get('metadata')) != {'atmem-document-policy': fingerprint(job), 'atmem-start-task-sha256': contract['startTaskSha256']}:
|
|
91
|
+
raise ValueError('Owned native pool policy changed')
|
|
92
|
+
exact = {'enableAutoScale': False, 'enableInterNodeCommunication': False, 'targetLowPriorityNodes': 0,
|
|
93
|
+
'currentLowPriorityNodes': 0, 'targetDedicatedNodes': contract['dedicatedNodes'],
|
|
94
|
+
'currentDedicatedNodes': contract['dedicatedNodes'], 'taskSlotsPerNode': contract['taskSlotsPerNode']}
|
|
95
|
+
if any(not typed_equal(value, observed.get(key)) for key, value in exact.items()) or observed.get('vmSize') != contract['vmSize']:
|
|
96
|
+
raise ValueError('Owned native pool scheduling or capacity changed')
|
|
97
|
+
for field in ['applicationPackageReferences', 'userAccounts', 'mountConfiguration', 'resizeErrors']:
|
|
98
|
+
if observed.get(field) not in [None, []]: raise ValueError('Unexpected shared pool input or resize error')
|
|
99
|
+
native_identity = observed.get('identity')
|
|
100
|
+
if not isinstance(native_identity, dict) or set(native_identity) != {'type', 'userAssignedIdentities'} or native_identity['type'] != 'UserAssigned':
|
|
101
|
+
raise ValueError('Use only the dedicated transfer identity on the node')
|
|
102
|
+
identities = native_identity['userAssignedIdentities']
|
|
103
|
+
if not isinstance(identities, list) or len(identities) != 1: raise ValueError('Do not expose other workload identities to the node')
|
|
104
|
+
identity = identities[0]
|
|
105
|
+
if (not isinstance(identity, dict) or set(identity) != {'resourceId', 'clientId', 'principalId'} or
|
|
106
|
+
not isinstance(identity['resourceId'], str) or identity['resourceId'].lower() != contract['identityResourceId'].lower() or
|
|
107
|
+
identity['clientId'] != job['transferClientId'] or not isinstance(identity['principalId'], str) or
|
|
108
|
+
not re.fullmatch(r'[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}', identity['principalId'])):
|
|
109
|
+
raise ValueError('Native pool managed identity changed')
|
|
110
|
+
network = observed.get('networkConfiguration')
|
|
111
|
+
if (not isinstance(network, dict) or not set(network) <= {'subnetId', 'publicIPAddressConfiguration', 'endpointConfiguration', 'enableAcceleratedNetworking'} or
|
|
112
|
+
network.get('subnetId', '').lower() != contract['subnetId'].lower() or
|
|
113
|
+
network.get('publicIPAddressConfiguration') != {'provision': 'nopublicipaddresses'} or
|
|
114
|
+
network.get('endpointConfiguration') not in [None, {'inboundNATPools': []}]):
|
|
115
|
+
raise ValueError('Use the exact same-subscription private subnet without public endpoints')
|
|
116
|
+
machine = observed.get('virtualMachineConfiguration')
|
|
117
|
+
if (not isinstance(machine, dict) or set(machine) != {'imageReference', 'nodeAgentSKUId', 'containerConfiguration', 'diskEncryptionConfiguration'} or
|
|
118
|
+
not image_matches(contract['imageReference'], machine['imageReference']) or machine['nodeAgentSKUId'] != contract['nodeAgentSkuId'] or
|
|
119
|
+
machine['diskEncryptionConfiguration'] != {'targets': ['osdisk', 'temporarydisk']}):
|
|
120
|
+
raise ValueError('Owned native image, agent SKU or disk encryption changed')
|
|
121
|
+
registry = job['parserImage'].split('/')[0]
|
|
122
|
+
expected = {'type': 'dockerCompatible', 'containerImageNames': [job['parserImage'], job['transferImage']],
|
|
123
|
+
'containerRegistries': [{'registryServer': registry, 'identityReference': {'resourceId': contract['identityResourceId']}}]}
|
|
124
|
+
if not typed_equal(expected, machine['containerConfiguration']):
|
|
125
|
+
raise ValueError('Use only the prefetched immutable images and dedicated registry identity')
|
|
126
|
+
verify_start_task(observed.get('startTask'), contract, job)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def verify_nodes(job, contract, response):
|
|
131
|
+
if (not isinstance(response, dict) or response.get('odata.nextLink') or response.get('@odata.nextLink') or
|
|
132
|
+
not isinstance(response.get('value'), list) or len(response['value']) != contract['dedicatedNodes']):
|
|
133
|
+
raise ValueError('Observe every node in the explicit dedicated pool')
|
|
134
|
+
identifiers = set()
|
|
135
|
+
for node in response['value']:
|
|
136
|
+
if (not isinstance(node, dict) or not isinstance(node.get('id'), str) or not re.fullmatch('[A-Za-z0-9_-]{1,128}', node['id']) or
|
|
137
|
+
node['id'] in identifiers or node.get('state') not in {'idle', 'running'} or node.get('schedulingState') != 'enabled' or
|
|
138
|
+
node.get('isDedicated') is not True or node.get('publicIPAddress') or node.get('publicIPv6Address') or node.get('errors') or
|
|
139
|
+
node.get('vmSize') != contract['vmSize']):
|
|
140
|
+
raise ValueError('Native dedicated node is not ready or private')
|
|
141
|
+
version = node.get('nodeAgentInfo', {}).get('version')
|
|
142
|
+
if not isinstance(version, str) or not re.fullmatch(r'[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}', version) or tuple(map(int, version.split('.'))) <= (1, 11, 11):
|
|
143
|
+
raise ValueError('Native node agent must support explicit empty container mounts')
|
|
144
|
+
startup = node.get('startTaskInfo', {})
|
|
145
|
+
if (startup.get('state') != 'completed' or type(startup.get('exitCode')) is not int or startup['exitCode'] != 0 or
|
|
146
|
+
startup.get('result') != 'success' or startup.get('failureInfo') or
|
|
147
|
+
not image_matches(contract['imageReference'], node.get('virtualMachineInfo', {}).get('imageReference'))):
|
|
148
|
+
raise ValueError('Native node did not complete the pinned installation on the pinned image')
|
|
149
|
+
verify_start_task(node.get('startTask'), contract, job)
|
|
150
|
+
identifiers.add(node['id'])
|
|
151
|
+
return frozenset(identifiers)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def preflight(job, contract, api):
|
|
155
|
+
job = job_configuration(job); contract = configuration(contract, job)
|
|
156
|
+
try:
|
|
157
|
+
api.account()
|
|
158
|
+
verify_pool(job, contract, api.pool())
|
|
159
|
+
return verify_nodes(job, contract, api.nodes())
|
|
160
|
+
except Exception:
|
|
161
|
+
raise SourceUnavailable('document_batch_pool_unavailable') from None
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Trusted Azure transfer stages; the untrusted parser never runs in this image."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
8
|
+
from enterprise_azure_objects import AzureDocumentObjects, etag
|
|
9
|
+
from enterprise_document_contract import MAX_INPUT, MEDIA_TYPES, media_type
|
|
10
|
+
from enterprise_document_job import MAX_ENVELOPE, read_file, request, result, unique
|
|
11
|
+
from enterprise_sources import SourceUnavailable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def metadata(encoded):
|
|
15
|
+
if not isinstance(encoded, str) or not 0 < len(encoded.encode()) <= 4096:
|
|
16
|
+
raise ValueError('Use bounded owned transfer metadata')
|
|
17
|
+
value = json.loads(encoded, object_pairs_hook=unique)
|
|
18
|
+
fields = {'format', 'subscriptionId', 'region', 'resourceGroup', 'storageAccount',
|
|
19
|
+
'container', 'transferClientId', 'request', 'inputETag'}
|
|
20
|
+
if not isinstance(value, dict) or set(value) != fields or value['format'] != 'enterprise-azure-document-transfer-v1':
|
|
21
|
+
raise ValueError('Use the exact Azure document transfer metadata')
|
|
22
|
+
# Constructing the native destination validates every identifier without
|
|
23
|
+
# acquiring a token or making an HTTP call. Close the unstarted session.
|
|
24
|
+
objects = document_objects(value)
|
|
25
|
+
objects.close()
|
|
26
|
+
expected = value['request']
|
|
27
|
+
if (not isinstance(expected, dict) or set(expected) != {'format', 'requestId', 'sha256', 'mediaType'} or
|
|
28
|
+
expected['format'] != 'enterprise-document-job-v1' or
|
|
29
|
+
not isinstance(expected['requestId'], str) or not re.fullmatch('[0-9a-f]{64}', expected['requestId']) or
|
|
30
|
+
not isinstance(expected['sha256'], str) or not re.fullmatch('[0-9a-f]{64}', expected['sha256']) or
|
|
31
|
+
media_type(expected['mediaType']) != expected['mediaType'] or
|
|
32
|
+
expected['mediaType'] not in MEDIA_TYPES | {'application/octet-stream'}):
|
|
33
|
+
raise ValueError('Use the exact supported byte-bound request')
|
|
34
|
+
try: etag(value['inputETag'])
|
|
35
|
+
except SourceUnavailable: raise ValueError('Use the acknowledged native input ETag') from None
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def document_objects(value):
|
|
40
|
+
return AzureDocumentObjects(subscription=value['subscriptionId'], resource_group=value['resourceGroup'],
|
|
41
|
+
region=value['region'], account=value['storageAccount'], container=value['container'], client_id=value['transferClientId'])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def directories(input_path, output_path):
|
|
45
|
+
source = target = None
|
|
46
|
+
try:
|
|
47
|
+
source = os.open(input_path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
48
|
+
target = os.open(output_path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
49
|
+
left, right = os.fstat(source), os.fstat(target)
|
|
50
|
+
if (left.st_dev, left.st_ino) == (right.st_dev, right.st_ino):
|
|
51
|
+
raise ValueError('Use distinct per-file volumes')
|
|
52
|
+
return source, target
|
|
53
|
+
except BaseException:
|
|
54
|
+
for fd in [target, source]:
|
|
55
|
+
if fd is not None: os.close(fd)
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def download(value, objects, input_path='/input', output_path='/output'):
|
|
60
|
+
source, target = directories(input_path, output_path)
|
|
61
|
+
try:
|
|
62
|
+
if os.listdir(source) or os.listdir(target):
|
|
63
|
+
raise ValueError('Use fresh per-file volumes')
|
|
64
|
+
expected = value['request']
|
|
65
|
+
identifier = expected['requestId']
|
|
66
|
+
raw = objects.load(identifier, 'input', value['inputETag'])
|
|
67
|
+
if request(raw, expected['mediaType'], identifier) != expected:
|
|
68
|
+
raise ValueError('Input bytes changed')
|
|
69
|
+
for name, data in [('document', raw), ('request.json', json.dumps(expected, separators=(',', ':')).encode())]:
|
|
70
|
+
fd = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o444, dir_fd=source)
|
|
71
|
+
with os.fdopen(fd, 'wb') as stream:
|
|
72
|
+
stream.write(data)
|
|
73
|
+
stream.flush()
|
|
74
|
+
os.fsync(stream.fileno())
|
|
75
|
+
os.fchmod(stream.fileno(), 0o444)
|
|
76
|
+
# Batch maps a native non-admin UID. Fresh isolated output is writable
|
|
77
|
+
# by that UID; only its parser and the trusted collector receive mounts.
|
|
78
|
+
os.fchmod(source, 0o555)
|
|
79
|
+
os.fchmod(target, 0o777)
|
|
80
|
+
os.fsync(source)
|
|
81
|
+
os.fsync(target)
|
|
82
|
+
finally:
|
|
83
|
+
os.close(target)
|
|
84
|
+
os.close(source)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def upload(value, objects, input_path='/input', output_path='/output'):
|
|
88
|
+
source, target = directories(input_path, output_path)
|
|
89
|
+
try:
|
|
90
|
+
expected = value['request']
|
|
91
|
+
identifier = expected['requestId']
|
|
92
|
+
raw = read_file(source, 'document', MAX_INPUT)
|
|
93
|
+
if request(raw, expected['mediaType'], identifier) != expected:
|
|
94
|
+
raise ValueError('Input binding changed')
|
|
95
|
+
encoded = read_file(target, 'result.json', MAX_ENVELOPE)
|
|
96
|
+
result(raw, expected['mediaType'], identifier, encoded)
|
|
97
|
+
objects.put(identifier, 'result', encoded)
|
|
98
|
+
finally:
|
|
99
|
+
os.close(target)
|
|
100
|
+
os.close(source)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main():
|
|
104
|
+
objects = None
|
|
105
|
+
try:
|
|
106
|
+
if len(sys.argv) != 2 or sys.argv[1] not in {'download', 'upload'}:
|
|
107
|
+
raise ValueError('Use the fixed Azure transfer stage')
|
|
108
|
+
value = metadata(os.environ.get('DOCUMENT_JOB', ''))
|
|
109
|
+
objects = document_objects(value)
|
|
110
|
+
if sys.argv[1] == 'download': download(value, objects)
|
|
111
|
+
else: upload(value, objects)
|
|
112
|
+
except Exception:
|
|
113
|
+
raise SystemExit('Owned Azure document transfer could not complete.') from None
|
|
114
|
+
finally:
|
|
115
|
+
if objects is not None:
|
|
116
|
+
try: objects.close()
|
|
117
|
+
except Exception:
|
|
118
|
+
raise SystemExit('Owned Azure document transfer could not complete.') from None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == '__main__':
|
|
122
|
+
main()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Owned private deployment metadata and native SQL identity dispatch.
|
|
2
|
+
|
|
3
|
+
No connection, cloud request, user enrollment or schema mutation occurs on import.
|
|
4
|
+
Metadata is packaged by the compiler/operator, never read from a chat request.
|
|
5
|
+
"""
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
from urllib.parse import urlsplit
|
|
12
|
+
from enterprise_identity import CompanyIdentity, trusted_https
|
|
13
|
+
from enterprise_sources import GraphSharePoint, ConfluenceCloud
|
|
14
|
+
from public_faq import object_pairs
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def deployment(value):
|
|
18
|
+
if (not isinstance(value, dict) or set(value) != {'format', 'provider', 'accountId', 'publicOrigin', 'company', 'sources', 'parserRevision'}
|
|
19
|
+
or value['format'] != 'enterprise-knowledge-v1' or value['provider'] not in {'aws', 'azure', 'gcp', 'ibm'}
|
|
20
|
+
or not isinstance(value['accountId'], str) or not re.fullmatch(r'[A-Za-z0-9_-]{1,128}', value['accountId'])
|
|
21
|
+
or not isinstance(value['parserRevision'], str) or not re.fullmatch(r'[0-9a-f]{64}', value['parserRevision'])):
|
|
22
|
+
raise ValueError('Invalid owned private deployment metadata')
|
|
23
|
+
origin = trusted_https(value['publicOrigin'])
|
|
24
|
+
if urlsplit(origin).path or origin.endswith('/'):
|
|
25
|
+
raise ValueError('Use the canonical owned HTTPS origin')
|
|
26
|
+
company = value['company']
|
|
27
|
+
if not isinstance(company, dict) or set(company) != {'issuer', 'audience', 'jwksUri', 'scope', 'web'}:
|
|
28
|
+
raise ValueError('Supply the existing company API and public OIDC registration')
|
|
29
|
+
identity = CompanyIdentity(value['accountId'], company['issuer'], company['audience'], company['jwksUri'], company['scope'])
|
|
30
|
+
from enterprise_web import company_configuration
|
|
31
|
+
company_configuration(identity, company['web'])
|
|
32
|
+
sources = value['sources']
|
|
33
|
+
if not isinstance(sources, dict) or set(sources) != {'sharepoint', 'confluence'}:
|
|
34
|
+
raise ValueError('Supply the existing SharePoint and Confluence registrations')
|
|
35
|
+
graph, confluence = sources['sharepoint'], sources['confluence']
|
|
36
|
+
if not isinstance(graph, dict) or set(graph) != {'tenantId', 'clientId', 'driveId', 'host'}:
|
|
37
|
+
raise ValueError('Invalid owned Graph registration')
|
|
38
|
+
if any(not isinstance(graph[key], str) or not re.fullmatch(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', graph[key]) for key in ['tenantId', 'clientId']):
|
|
39
|
+
raise ValueError('Use the owned Graph tenant and confidential client IDs')
|
|
40
|
+
if (not isinstance(confluence, dict) or set(confluence) != {'clientId', 'cloudId', 'host', 'spaces'}
|
|
41
|
+
or not isinstance(confluence['clientId'], str) or not re.fullmatch(r'[A-Za-z0-9_-]{1,200}', confluence['clientId'])):
|
|
42
|
+
raise ValueError('Invalid owned Confluence registration')
|
|
43
|
+
# Constructor validation is local only: no delegated access is used here.
|
|
44
|
+
GraphSharePoint(graph['driveId'], graph['host'], None)
|
|
45
|
+
ConfluenceCloud(confluence['cloudId'], confluence['host'], confluence['spaces'], None)
|
|
46
|
+
return json.loads(json.dumps(value, allow_nan=False))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def read_owned_json(filename,direct,pointer,environment,*,path=None):
|
|
50
|
+
"""Compiler/operator metadata only; no chat/request chooses a file or loader."""
|
|
51
|
+
if (direct in environment and pointer in environment)or(path is not None and any(key in environment for key in [direct,pointer])):
|
|
52
|
+
raise ValueError('Conflicting owned configuration sources')
|
|
53
|
+
if direct in environment:
|
|
54
|
+
value=environment[direct]
|
|
55
|
+
if not isinstance(value,str):raise ValueError('Use serialized owned configuration')
|
|
56
|
+
raw=value.encode('utf-8')
|
|
57
|
+
else:
|
|
58
|
+
if pointer in environment:
|
|
59
|
+
value=environment[pointer]
|
|
60
|
+
if not isinstance(value,str)or not Path(value).is_absolute():raise ValueError('Use an absolute owned configuration mount')
|
|
61
|
+
path=Path(value)
|
|
62
|
+
path=Path(path)if path is not None else Path(__file__).with_name(filename)
|
|
63
|
+
with path.open('rb')as stream:raw=stream.read(32769)
|
|
64
|
+
if not 0<len(raw)<=32768:raise ValueError('Owned configuration exceeds its byte boundary')
|
|
65
|
+
def constant(value):raise ValueError('Use finite owned JSON values')
|
|
66
|
+
return json.loads(raw,object_pairs_hook=object_pairs,parse_constant=constant)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def read_deployment(path=None,*,environment=None):
|
|
70
|
+
environment=os.environ if environment is None else environment
|
|
71
|
+
return deployment(read_owned_json('enterprise-deployment.json','ENTERPRISE_DEPLOYMENT_JSON','ENTERPRISE_DEPLOYMENT_FILE',environment,path=path))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def session_domain(metadata):
|
|
75
|
+
return hashlib.sha256(json.dumps({'account':metadata['accountId'],'origin':metadata['publicOrigin'],
|
|
76
|
+
'company':metadata['company'],'sources':metadata['sources'],'provider':metadata['provider']},sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def native_database(provider, environment, role, secrets):
|
|
80
|
+
from workflow_connection import aws_database, azure_database, gcp_database, ibm_database
|
|
81
|
+
if role not in {'api', 'worker', 'migration'}:
|
|
82
|
+
raise ValueError('Use the owned native database role')
|
|
83
|
+
env = environment
|
|
84
|
+
common = {'database': env['DATABASE_NAME'], 'role': role}
|
|
85
|
+
if provider == 'gcp':
|
|
86
|
+
transport = env.get('DATABASE_TRANSPORT', 'cloudsql-socket')
|
|
87
|
+
if transport not in {'cloudsql-socket', 'cloudsql-private-proxy'}:
|
|
88
|
+
raise ValueError('Use the owned Cloud SQL transport')
|
|
89
|
+
return gcp_database(**common, connection_name=env['DATABASE_CONNECTION_NAME'],
|
|
90
|
+
service_account_email=env['DATABASE_SERVICE_ACCOUNT'], private_proxy=transport == 'cloudsql-private-proxy')
|
|
91
|
+
common.update(host=env['DATABASE_HOST'], user=env['DATABASE_USER'], ca_file=secrets.env['DATABASE_CA_FILE'])
|
|
92
|
+
if provider == 'aws': return aws_database(**common, region=env['REGION'])
|
|
93
|
+
if provider == 'azure': return azure_database(**common, managed_identity_client_id=env['WORKLOAD_IDENTITY_CLIENT_ID'])
|
|
94
|
+
if provider == 'ibm': return ibm_database(**common, port=int(env['DATABASE_PORT']), password=secrets.reader('database_password'))
|
|
95
|
+
raise ValueError('Unsupported owned database provider')
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def validate_runtime(auth, role, account_id):
|
|
99
|
+
if role not in {'api', 'worker'}: raise ValueError('Use a runtime database identity')
|
|
100
|
+
schemas = {'enterprise_auth': 'enterprise_auth_schema.sql', 'enterprise_queries': 'enterprise_queries_schema.sql',
|
|
101
|
+
'enterprise_audit': 'enterprise_audit_schema.sql'}
|
|
102
|
+
if role == 'worker':
|
|
103
|
+
schemas['enterprise_extractions'] = 'enterprise_extraction_schema.sql'
|
|
104
|
+
schemas['enterprise_document_jobs'] = 'enterprise_document_jobs_schema.sql'
|
|
105
|
+
with auth.transaction() as cursor:
|
|
106
|
+
row = cursor.execute('SELECT rolsuper,rolcreatedb,rolcreaterole,rolreplication,rolbypassrls FROM pg_roles WHERE rolname=current_user').fetchone()
|
|
107
|
+
if not row or any(row.values()): raise ValueError('Private runtime must use its nonadministrative database identity')
|
|
108
|
+
for schema, filename in schemas.items():
|
|
109
|
+
if cursor.execute('SELECT has_schema_privilege(current_user,%s,\'CREATE\') AS allowed', (schema,)).fetchone()['allowed']:
|
|
110
|
+
raise ValueError('Runtime must not own private application schemas')
|
|
111
|
+
# Identifiers come only from the fixed dictionary above.
|
|
112
|
+
current = cursor.execute('SELECT version,source_sha256 FROM ' + schema + '.schema_version').fetchone()
|
|
113
|
+
expected = hashlib.sha256(Path(__file__).with_name(filename).read_bytes()).hexdigest()
|
|
114
|
+
if current != {'version': 1, 'source_sha256': expected}:
|
|
115
|
+
raise ValueError('Private runtime schema differs; run the reviewed operator lifecycle')
|
|
116
|
+
queue = cursor.execute('SELECT queue_version FROM enterprise_queries.schema_version').fetchone()
|
|
117
|
+
if queue['queue_version'] != '3.10.0': raise ValueError('Private runtime queue version differs')
|
|
118
|
+
for table in ['enterprise_auth.memberships', 'enterprise_auth.source_mappings', 'enterprise_auth.company_bindings', 'enterprise_auth.schema_version']:
|
|
119
|
+
if cursor.execute('SELECT has_table_privilege(current_user,%s,\'INSERT,UPDATE,DELETE,TRUNCATE,TRIGGER,REFERENCES\') AS allowed', (table,)).fetchone()['allowed']:
|
|
120
|
+
raise ValueError('Runtime must not inherit company authority writes')
|
|
121
|
+
if cursor.execute("SELECT has_table_privilege(current_user,'enterprise_audit.events','SELECT,UPDATE,DELETE,TRUNCATE,TRIGGER,REFERENCES') AS allowed").fetchone()['allowed']:
|
|
122
|
+
raise ValueError('Runtime must use append-only audit authority')
|
|
123
|
+
if cursor.execute("SELECT has_table_privilege(current_user,'enterprise_auth.administration_events','SELECT,INSERT,UPDATE,DELETE,TRUNCATE,TRIGGER,REFERENCES') AS allowed").fetchone()['allowed']:
|
|
124
|
+
raise ValueError('Runtime must not inherit operator history authority')
|
|
125
|
+
if role == 'api' and cursor.execute("SELECT has_table_privilege(current_user,'enterprise_queries.procrastinate_jobs','UPDATE,DELETE,TRUNCATE') AS allowed").fetchone()['allowed']:
|
|
126
|
+
raise ValueError('API must not inherit worker queue mutation authority')
|
|
127
|
+
forbidden = 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE,TRIGGER,REFERENCES' if role == 'api' else 'TRUNCATE,TRIGGER,REFERENCES'
|
|
128
|
+
table = cursor.execute("SELECT c.oid::bigint AS oid FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname='enterprise_document_jobs'AND c.relname='jobs'AND c.relkind='r'").fetchone()
|
|
129
|
+
if not table:raise ValueError('Private document cleanup schema is missing')
|
|
130
|
+
# API deliberately has no schema USAGE. Resolve the object through the
|
|
131
|
+
# public system catalogue instead of broadening its application grants.
|
|
132
|
+
if cursor.execute("SELECT has_table_privilege(current_user,%s::oid,%s) AS allowed", (table['oid'],forbidden)).fetchone()['allowed']:
|
|
133
|
+
raise ValueError('Use only the owned worker cleanup metadata authority')
|
|
134
|
+
from enterprise_sources import SourceDenied
|
|
135
|
+
try:auth.require_company(cursor,account_id)
|
|
136
|
+
except SourceDenied:raise ValueError('Activate the owned company registration with the reviewed operator lifecycle')from None
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""General company-document decoding in a separate credentialless parser service.
|
|
2
|
+
|
|
3
|
+
This module accepts bytes, never caller-selected URLs or parser configuration.
|
|
4
|
+
The JVM/native tools must be contained by the deployment's network/resource sandbox;
|
|
5
|
+
process deadlines alone are not isolation. Invoice PDF protocol is unchanged.
|
|
6
|
+
"""
|
|
7
|
+
import base64
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import selectors
|
|
12
|
+
import signal
|
|
13
|
+
import subprocess
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
from enterprise_document_contract import MAX_INPUT, MAX_TEXT, MEDIA_TYPES, ExtractionError, media_type, document_result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CompanyDocument:
|
|
21
|
+
def __init__(self, *, java, classpath, pdf, timeout=30):
|
|
22
|
+
if not Path(java).is_absolute() or not Path(java).is_file():
|
|
23
|
+
raise ValueError('Supply the installed pinned Java executable')
|
|
24
|
+
if not isinstance(classpath, (list, tuple)) or not classpath or any(not Path(p).is_absolute() or not Path(p).exists() for p in classpath):
|
|
25
|
+
raise ValueError('Supply the fixed bridge and verified Tika classpath')
|
|
26
|
+
if not callable(getattr(pdf, 'extract', None)) or type(timeout) is not int or not 1 <= timeout <= 110:
|
|
27
|
+
raise ValueError('Supply the bounded native PDF extractor and timeout')
|
|
28
|
+
self.java = java
|
|
29
|
+
self.classpath = os.pathsep.join(str(p) for p in classpath)
|
|
30
|
+
self.pdf = pdf
|
|
31
|
+
self.timeout = timeout
|
|
32
|
+
|
|
33
|
+
def extract(self, raw, declared):
|
|
34
|
+
if not isinstance(raw, bytes) or not raw:
|
|
35
|
+
raise ExtractionError('invalid_document')
|
|
36
|
+
if len(raw) > MAX_INPUT:
|
|
37
|
+
raise ExtractionError('document_too_large')
|
|
38
|
+
declared = media_type(declared)
|
|
39
|
+
digest = hashlib.sha256(raw).hexdigest()
|
|
40
|
+
if raw.startswith(b'%PDF-'):
|
|
41
|
+
from invoice_document import DocumentError
|
|
42
|
+
try:
|
|
43
|
+
parsed = self.pdf.extract(raw, digest)
|
|
44
|
+
except DocumentError as failure:
|
|
45
|
+
raise ExtractionError('extraction_timeout' if failure.code == 'extraction_timeout' else 'extraction_failed') from None
|
|
46
|
+
pages = []
|
|
47
|
+
for page in parsed['pages']:
|
|
48
|
+
native, ocr = page['text'].strip(), page['ocr'].strip()
|
|
49
|
+
# Both channels matter for mixed text/image pages. Do not discard
|
|
50
|
+
# image-only facts because a page also has a native-text header.
|
|
51
|
+
body = native if native == ocr else '\n'.join(part for part in (native, ocr) if part)
|
|
52
|
+
pages.append(f"Page {page['number']}\n{body}")
|
|
53
|
+
detected, text = 'application/pdf', '\n\n'.join(pages)
|
|
54
|
+
else:
|
|
55
|
+
detected, text = self._tika(raw)
|
|
56
|
+
return document_result({'format': 'enterprise-document-v1', 'sha256': digest,
|
|
57
|
+
'mediaType': detected, 'text': text}, raw, declared)
|
|
58
|
+
|
|
59
|
+
def _tika(self, raw):
|
|
60
|
+
with tempfile.TemporaryDirectory(prefix='atmem-company-document-') as temporary:
|
|
61
|
+
folder = Path(temporary)
|
|
62
|
+
source = folder / 'input'
|
|
63
|
+
source.write_bytes(raw)
|
|
64
|
+
source.chmod(0o600)
|
|
65
|
+
process = None
|
|
66
|
+
try:
|
|
67
|
+
# No JAVA_TOOL_OPTIONS, CLASSPATH, proxy, application or workload
|
|
68
|
+
# credentials enter the parser. Child tools share the outer group.
|
|
69
|
+
process = subprocess.Popen([self.java, '-Xmx256m', '-XX:ActiveProcessorCount=1',
|
|
70
|
+
'-Djava.awt.headless=true', '-Djava.io.tmpdir=' + temporary,
|
|
71
|
+
'-cp', self.classpath, 'EnterpriseDocument', str(source)],
|
|
72
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
73
|
+
cwd=folder, start_new_session=True, env={'PATH': '/usr/local/bin:/usr/bin:/bin', 'HOME': temporary,
|
|
74
|
+
'TMPDIR': temporary, 'LANG': 'C.UTF-8', 'OMP_THREAD_LIMIT': '1',
|
|
75
|
+
'TESSDATA_PREFIX': '/usr/share/tesseract-ocr/5/tessdata'})
|
|
76
|
+
deadline = time.monotonic() + self.timeout
|
|
77
|
+
output = bytearray()
|
|
78
|
+
with selectors.DefaultSelector() as selector:
|
|
79
|
+
os.set_blocking(process.stdout.fileno(), False)
|
|
80
|
+
selector.register(process.stdout, selectors.EVENT_READ)
|
|
81
|
+
while selector.get_map():
|
|
82
|
+
remaining = deadline - time.monotonic()
|
|
83
|
+
if remaining <= 0:
|
|
84
|
+
raise ExtractionError('extraction_timeout')
|
|
85
|
+
for key, _ in selector.select(min(remaining, .1)):
|
|
86
|
+
chunk = os.read(key.fd, 8192)
|
|
87
|
+
if not chunk:
|
|
88
|
+
selector.unregister(key.fileobj)
|
|
89
|
+
else:
|
|
90
|
+
output.extend(chunk)
|
|
91
|
+
if len(output) > 270000:
|
|
92
|
+
raise ExtractionError('extraction_failed')
|
|
93
|
+
try:
|
|
94
|
+
code = process.wait(timeout=max(.01, deadline - time.monotonic()))
|
|
95
|
+
except subprocess.TimeoutExpired:
|
|
96
|
+
raise ExtractionError('extraction_timeout') from None
|
|
97
|
+
lines = output.decode('ascii').splitlines()
|
|
98
|
+
if code == 2 and lines == ['rejected', 'unsupported_format']:
|
|
99
|
+
raise ExtractionError('unsupported_format')
|
|
100
|
+
if code != 0 or len(lines) != 3 or lines[0] != 'ok':
|
|
101
|
+
raise ExtractionError('extraction_failed')
|
|
102
|
+
return lines[1], base64.b64decode(lines[2], validate=True).decode('utf-8')
|
|
103
|
+
except ExtractionError:
|
|
104
|
+
raise
|
|
105
|
+
except Exception:
|
|
106
|
+
raise ExtractionError('extraction_unavailable') from None
|
|
107
|
+
finally:
|
|
108
|
+
if process is not None:
|
|
109
|
+
# Kill the entire private group, including an OCR child that
|
|
110
|
+
# outlived a failed/crashed JVM. Never leave document workers.
|
|
111
|
+
try:
|
|
112
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
113
|
+
except ProcessLookupError:
|
|
114
|
+
pass
|
|
115
|
+
process.wait()
|
|
116
|
+
process.stdout.close()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Byte-only CLI used by the isolated document parser image."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
6
|
+
from enterprise_document import CompanyDocument, ExtractionError, MAX_INPUT
|
|
7
|
+
from invoice_document import PdfDocument
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def installed_parser():
|
|
11
|
+
return CompanyDocument(java='/opt/java/bin/java',
|
|
12
|
+
classpath=['/app/tika-app.jar', '/app/document-bridge'], pdf=PdfDocument())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == '__main__':
|
|
16
|
+
if sys.argv[1:] == ['--job']:
|
|
17
|
+
from enterprise_document_job import execute
|
|
18
|
+
value, code = execute(installed_parser())
|
|
19
|
+
print(json.dumps(value, allow_nan=False))
|
|
20
|
+
raise SystemExit(code)
|
|
21
|
+
try:
|
|
22
|
+
if len(sys.argv) > 2:
|
|
23
|
+
raise ExtractionError('invalid_document')
|
|
24
|
+
raw = sys.stdin.buffer.read(MAX_INPUT + 1)
|
|
25
|
+
document = installed_parser().extract(raw, sys.argv[1] if len(sys.argv) == 2 else 'application/octet-stream')
|
|
26
|
+
value, code = {'status': 'ok', 'document': document}, 0
|
|
27
|
+
except ExtractionError as failure:
|
|
28
|
+
value, code = {'status': 'rejected', 'reason': failure.code}, 2
|
|
29
|
+
except Exception:
|
|
30
|
+
value, code = {'status': 'rejected', 'reason': 'extraction_unavailable'}, 2
|
|
31
|
+
print(json.dumps(value, ensure_ascii=False, allow_nan=False))
|
|
32
|
+
raise SystemExit(code)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Byte-bound document protocol shared by app/cache and isolated parser.
|
|
2
|
+
|
|
3
|
+
Standard-library validation only: no subprocess, native parser, URL or secrets.
|
|
4
|
+
"""
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
MAX_INPUT = 10000000
|
|
9
|
+
MAX_TEXT = 200000
|
|
10
|
+
MEDIA_TYPES = frozenset({'application/pdf', 'text/plain', 'text/html', 'application/rtf',
|
|
11
|
+
'application/msword', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint',
|
|
12
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
13
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
14
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
15
|
+
'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet',
|
|
16
|
+
'application/vnd.oasis.opendocument.presentation'})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExtractionError(ValueError):
|
|
20
|
+
def __init__(self, code):
|
|
21
|
+
if code not in {'document_too_large', 'invalid_document', 'unsupported_format',
|
|
22
|
+
'extraction_failed', 'extraction_timeout', 'extraction_unavailable', 'mime_mismatch'}:
|
|
23
|
+
raise ValueError('Unknown extraction failure')
|
|
24
|
+
self.code = code
|
|
25
|
+
super().__init__(code)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def media_type(value):
|
|
29
|
+
if not isinstance(value, str) or len(value) > 200 or any(ord(c) < 32 for c in value):
|
|
30
|
+
raise ExtractionError('invalid_document')
|
|
31
|
+
result = value.split(';', 1)[0].strip().lower()
|
|
32
|
+
if not re.fullmatch(r'[a-z0-9.+-]+/[a-z0-9.+-]+', result):
|
|
33
|
+
raise ExtractionError('invalid_document')
|
|
34
|
+
return {'text/rtf': 'application/rtf', 'application/x-rtf': 'application/rtf'}.get(result, result)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def document_result(value, raw, declared):
|
|
38
|
+
"""Validate the entire byte-bound response at both sides of the RPC boundary."""
|
|
39
|
+
if not isinstance(raw, bytes) or not 0 < len(raw) <= MAX_INPUT:
|
|
40
|
+
raise ExtractionError('invalid_document')
|
|
41
|
+
if not isinstance(value, dict) or set(value) != {'format', 'sha256', 'mediaType', 'text'}:
|
|
42
|
+
raise ExtractionError('invalid_document')
|
|
43
|
+
if value['format'] != 'enterprise-document-v1' or value['sha256'] != hashlib.sha256(raw).hexdigest():
|
|
44
|
+
raise ExtractionError('invalid_document')
|
|
45
|
+
actual = media_type(value['mediaType'])
|
|
46
|
+
if actual not in MEDIA_TYPES or actual != value['mediaType']:
|
|
47
|
+
raise ExtractionError('unsupported_format')
|
|
48
|
+
expected = media_type(declared)
|
|
49
|
+
if actual != expected and expected != 'application/octet-stream':
|
|
50
|
+
raise ExtractionError('mime_mismatch')
|
|
51
|
+
text = value['text']
|
|
52
|
+
if not isinstance(text, str) or not text.strip() or len(text.encode('utf-8')) > MAX_TEXT or '\x00' in text:
|
|
53
|
+
raise ExtractionError('invalid_document')
|
|
54
|
+
return value
|