@atmem/architect 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONSTITUTION.md +144 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +31 -2
- package/agent-skills/discovery/SKILL.md +48 -0
- package/agent-skills/discovery/topics.json +139 -0
- package/agent-skills/generation-capabilities.json +760 -0
- package/agent-skills/service-lifecycle.json +21 -0
- package/agent-skills/specialists/architecture/SKILL.md +15 -0
- package/agent-skills/specialists/aws/SKILL.md +17 -0
- package/agent-skills/specialists/azure/SKILL.md +7 -0
- package/agent-skills/specialists/composition/SKILL.md +17 -0
- package/agent-skills/specialists/devops/SKILL.md +10 -0
- package/agent-skills/specialists/finops/SKILL.md +7 -0
- package/agent-skills/specialists/gcp/SKILL.md +7 -0
- package/agent-skills/specialists/ibm/SKILL.md +7 -0
- package/agent-skills/specialists/manifest.json +13 -0
- package/agent-skills/specialists/pulumi/SKILL.md +7 -0
- package/agent-skills/specialists/security/SKILL.md +7 -0
- package/agent-skills/specialists/terraform/SKILL.md +14 -0
- package/agent-skills/upstream/aws/LICENSE +175 -0
- package/agent-skills/upstream/aws/SKILL.md +125 -0
- package/agent-skills/upstream/azure/LICENSE +21 -0
- package/agent-skills/upstream/azure/SKILL.md +387 -0
- package/agent-skills/upstream/gcp/LICENSE +202 -0
- package/agent-skills/upstream/gcp/SKILL.md +159 -0
- package/agent-skills/upstream/manifest.json +57 -0
- package/agent-skills/upstream/pulumi/LICENSE +201 -0
- package/agent-skills/upstream/pulumi/SKILL.md +501 -0
- package/agent-skills/upstream/terraform/LICENSE +373 -0
- package/agent-skills/upstream/terraform/SKILL.md +316 -0
- package/agent-skills/workflows/workflows.json +601 -0
- package/cli/architect.mjs +3 -0
- package/cli/runtime.mjs +115 -0
- package/dist/assets/html2canvas.esm-QH1iLAAe.js +22 -0
- package/dist/assets/index-BTLHaa3J.css +1 -0
- package/dist/assets/index-CQ2C1gKF.js +21338 -0
- package/dist/assets/index.es-DTyqIdaO.js +18 -0
- package/dist/assets/purify.es-CAi2OJFm.js +3 -0
- package/dist/assets/visual-export-Ox0j5xj5.js +173 -0
- package/dist/favicon.svg +2 -0
- package/dist/fonts/DM-Sans-OFL.txt +93 -0
- package/dist/fonts/Manrope-OFL.txt +93 -0
- package/dist/fonts/NotoSans-Regular.ttf +0 -0
- package/dist/fonts/OFL.txt +94 -0
- package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/index.html +4 -0
- package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
- package/dist/licenses/aws-architect-LICENSE.txt +17 -0
- package/dist/licenses/aws-mcp-LICENSE.txt +175 -0
- package/dist/licenses/aws-mcp-NOTICE.txt +1 -0
- package/dist/licenses/cloud-emulation-notices.txt +36619 -0
- package/dist/licenses/dependency-notices.txt +6379 -0
- package/dist/licenses/emulation-notices.txt +717 -0
- package/dist/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/dist/licenses/memory-dependency-notices.txt +3319 -0
- package/dist/licenses/openarchflow-LICENSE.txt +21 -0
- package/dist/licenses/python-dependency-notices.txt +11951 -0
- package/docs/upstream-lock.json +26 -0
- package/knowledge/engineering-practices.json +536 -0
- package/package.json +30 -4
- package/packaging/uv-release-lock.json +54 -0
- package/public/licenses/atmem-APACHE-2.0.txt +202 -0
- package/public/licenses/aws-architect-LICENSE.txt +17 -0
- package/public/licenses/aws-mcp-LICENSE.txt +175 -0
- package/public/licenses/aws-mcp-NOTICE.txt +1 -0
- package/public/licenses/cloud-emulation-notices.txt +36619 -0
- package/public/licenses/dependency-notices.txt +6379 -0
- package/public/licenses/emulation-notices.txt +717 -0
- package/public/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/public/licenses/memory-dependency-notices.txt +3319 -0
- package/public/licenses/openarchflow-LICENSE.txt +21 -0
- package/public/licenses/python-dependency-notices.txt +11951 -0
- package/scripts/terraform-render.mjs +8 -0
- package/server/__init__.py +0 -0
- package/server/abstraction.py +28 -0
- package/server/agent_runtime.py +153 -0
- package/server/app.py +257 -0
- package/server/architect.py +112 -0
- package/server/architecture_activity.py +67 -0
- package/server/attachments.py +101 -0
- package/server/cloud-emulation-requirements.txt +29 -0
- package/server/codex_app_server.py +219 -0
- package/server/codex_engineering_backend.py +266 -0
- package/server/codex_worker_policy.py +193 -0
- package/server/contracts.py +315 -0
- package/server/copilot_worker.py +16 -0
- package/server/dependency_mirror.py +54 -0
- package/server/design_patch.py +43 -0
- package/server/emulation-requirements.txt +8 -0
- package/server/emulation_adapters.py +20 -0
- package/server/emulation_routes.py +98 -0
- package/server/emulation_worker.py +351 -0
- package/server/engineering_artifacts.py +221 -0
- package/server/engineering_assets/application-runtime.json +4372 -0
- package/server/engineering_assets/architecture_work.schema.json +79 -0
- package/server/engineering_assets/materialize_runtime.py +62 -0
- package/server/engineering_assets/prepare_tools.py +96 -0
- package/server/engineering_assets/sql_validation.py +59 -0
- package/server/engineering_contexts.py +120 -0
- package/server/engineering_delivery.py +210 -0
- package/server/engineering_finalization.py +210 -0
- package/server/engineering_handoff.py +172 -0
- package/server/engineering_jobs.py +206 -0
- package/server/engineering_knowledge.py +75 -0
- package/server/engineering_reconciliation.py +159 -0
- package/server/engineering_routes.py +483 -0
- package/server/engineering_sessions.py +273 -0
- package/server/engineering_tools.py +166 -0
- package/server/generation_lint.py +18 -0
- package/server/local_providers.py +185 -0
- package/server/local_validation.py +236 -0
- package/server/memory-requirements.lock +181 -0
- package/server/memory-requirements.txt +6 -0
- package/server/memory_context.py +58 -0
- package/server/memory_routes.py +141 -0
- package/server/memory_service.py +306 -0
- package/server/memory_worker.py +129 -0
- package/server/openai_voice.py +80 -0
- package/server/orca_handoff.py +90 -0
- package/server/packaged_app.py +9 -0
- package/server/pricing.py +190 -0
- package/server/process_control_observation.py +31 -0
- package/server/prompts.py +136 -0
- package/server/public_faq_emulation_worker.py +44 -0
- package/server/requirements.lock +1618 -0
- package/server/requirements.txt +68 -0
- package/server/runtime_dependency_locks.py +27 -0
- package/server/runtime_evidence.py +93 -0
- package/server/settings.py +14 -0
- package/server/skills.py +103 -0
- package/server/storage_emulation_worker.py +160 -0
- package/server/structured_emulation_worker.py +48 -0
- package/server/terraform_routes.py +118 -0
- package/server/upstream/__init__.py +0 -0
- package/server/upstream/agent/__init__.py +1 -0
- package/server/upstream/agent/aws_architect.py +600 -0
- package/server/upstream/agent/config/__init__.py +1 -0
- package/server/upstream/agent/config/agent_config.py +82 -0
- package/server/upstream/agent/config/prompts.py +138 -0
- package/server/upstream/agent/rate_limiter.py +62 -0
- package/server/upstream/utils/__init__.py +1 -0
- package/server/upstream/utils/file_writer.py +43 -0
- package/server/validation_applicability.py +85 -0
- package/server/validation_jobs.py +252 -0
- package/server/validation_workers.py +48 -0
- package/server/voice_routes.py +91 -0
- package/server/voice_worker.py +26 -0
- package/server/windows_snapshot.py +119 -0
- package/specs/028-convergence-evaluation/evidence/20261110-ibm-invoice-native/registry-structured/qualification.json +72 -0
- package/specs/028-convergence-evaluation/evidence/20261110-invoice-runtime/durable-current/receipt.json +61 -0
- package/specs/029-general-purpose-engine/evidence/20261111-runtime-dependencies/sdk/receipt.json +22 -0
- package/src/ArchitectureActivity.jsx +54 -0
- package/src/ArchitectureWorkPanel.jsx +39 -0
- package/src/ArtifactChecks.jsx +49 -0
- package/src/BlockRequirements.jsx +13 -0
- package/src/BrainActivity.jsx +11 -0
- package/src/ChatMessage.jsx +29 -0
- package/src/ChatResizeHandle.jsx +6 -0
- package/src/CodePreview.jsx +8 -0
- package/src/ComponentReview.jsx +31 -0
- package/src/DeliveryViews.jsx +59 -0
- package/src/EmulationPanel.jsx +44 -0
- package/src/EngagementViews.jsx +387 -0
- package/src/EngineeringPanel.jsx +151 -0
- package/src/FileTree.jsx +7 -0
- package/src/FinalChecklist.jsx +15 -0
- package/src/HelpNote.jsx +3 -0
- package/src/ImplementationDelivery.jsx +91 -0
- package/src/JourneyViews.jsx +403 -0
- package/src/LocationChoices.jsx +12 -0
- package/src/MemoryPanel.jsx +58 -0
- package/src/OpenAIVoicePanel.jsx +66 -0
- package/src/OutputsView.jsx +71 -0
- package/src/ProjectChecks.jsx +44 -0
- package/src/TerraformView.jsx +57 -0
- package/src/WorkbenchDialogs.jsx +532 -0
- package/src/activity-presentation.mjs +28 -0
- package/src/ai-qualification-scenarios.mjs +31 -0
- package/src/approval-application-files.mjs +32 -0
- package/src/approval-applications.mjs +82 -0
- package/src/approval-spec-kit.mjs +30 -0
- package/src/architecture-activity.css +28 -0
- package/src/architecture-activity.mjs +63 -0
- package/src/architecture-work.mjs +176 -0
- package/src/artifact-work.mjs +103 -0
- package/src/async-documents.mjs +191 -0
- package/src/atflow.css +1140 -0
- package/src/atforma.css +748 -0
- package/src/atmem-tokens.css +79 -0
- package/src/aws-preflight.mjs +60 -0
- package/src/aws-public-faq.mjs +102 -0
- package/src/aws-validation-scenarios.mjs +10 -0
- package/src/aws-validation.mjs +48 -0
- package/src/azure-plan-mocks.mjs +25 -0
- package/src/azure-structured.mjs +133 -0
- package/src/bedrock-summary.mjs +152 -0
- package/src/bounded-rag.mjs +60 -0
- package/src/brand.css +9 -0
- package/src/canvas-view.mjs +28 -0
- package/src/capability-composition.mjs +44 -0
- package/src/chat-attachments.mjs +13 -0
- package/src/cloud-provider-locks.mjs +9 -0
- package/src/cloud-storage-scenarios.mjs +2 -0
- package/src/component-review.mjs +55 -0
- package/src/composition-spec-kit.mjs +30 -0
- package/src/consultation-context.mjs +15 -0
- package/src/consultation.mjs +52 -0
- package/src/container-contracts.mjs +5 -0
- package/src/container-inspection.mjs +75 -0
- package/src/container-runtime.mjs +30 -0
- package/src/container-security-inspection.mjs +49 -0
- package/src/container-terraform.mjs +109 -0
- package/src/conversation.mjs +18 -0
- package/src/core.mjs +863 -0
- package/src/cross-cloud-storage.mjs +100 -0
- package/src/delivery-capabilities.mjs +25 -0
- package/src/delivery.mjs +58 -0
- package/src/deployment-ownership.mjs +10 -0
- package/src/deployment-region.mjs +11 -0
- package/src/derive.mjs +31 -0
- package/src/design-build-flow.mjs +10 -0
- package/src/diagram-attention.mjs +24 -0
- package/src/discovery.mjs +359 -0
- package/src/document.mjs +170 -0
- package/src/emulation-feedback.mjs +61 -0
- package/src/emulation-guidance.mjs +37 -0
- package/src/engagement.mjs +274 -0
- package/src/engineering-client.mjs +73 -0
- package/src/engineering-defaults.mjs +17 -0
- package/src/engineering.css +31 -0
- package/src/enterprise-application.mjs +76 -0
- package/src/enterprise-aws-host-infrastructure.mjs +62 -0
- package/src/enterprise-aws-host-tests.mjs +15 -0
- package/src/enterprise-aws-infrastructure-tests.mjs +38 -0
- package/src/enterprise-aws-infrastructure.mjs +138 -0
- package/src/enterprise-google-infrastructure.mjs +84 -0
- package/src/enterprise-google-tests.mjs +25 -0
- package/src/estimate.mjs +190 -0
- package/src/existing-references.mjs +18 -0
- package/src/external-identities.mjs +17 -0
- package/src/faq-lambda-build.mjs +28 -0
- package/src/final-package.mjs +34 -0
- package/src/gcp-structured.mjs +104 -0
- package/src/generation-provider.mjs +7 -0
- package/src/generation-quality.mjs +69 -0
- package/src/handoff.mjs +141 -0
- package/src/ibm-registry.mjs +12 -0
- package/src/ibm-structured.mjs +103 -0
- package/src/identifiers.mjs +15 -0
- package/src/implementation-map.mjs +14 -0
- package/src/implementation-outcome.mjs +8 -0
- package/src/incomplete-terraform.mjs +9 -0
- package/src/integrations.ts +137 -0
- package/src/interface-contracts.mjs +30 -0
- package/src/invoice-application.mjs +66 -0
- package/src/invoice-aws-operations.mjs +136 -0
- package/src/invoice-aws.mjs +143 -0
- package/src/invoice-azure-operations.mjs +147 -0
- package/src/invoice-azure.mjs +134 -0
- package/src/invoice-builder.mjs +35 -0
- package/src/invoice-dependencies.mjs +43 -0
- package/src/invoice-gcp-operations.mjs +178 -0
- package/src/invoice-gcp.mjs +104 -0
- package/src/invoice-ibm-operations.mjs +278 -0
- package/src/invoice-ibm.mjs +142 -0
- package/src/invoice-operations.mjs +143 -0
- package/src/invoice-plan.mjs +45 -0
- package/src/invoice-region.mjs +6 -0
- package/src/invoice-spec-kit.mjs +61 -0
- package/src/invoice-state.mjs +7 -0
- package/src/invoice-terraform.mjs +29 -0
- package/src/invoice-tests.mjs +122 -0
- package/src/journey-workspace.css +106 -0
- package/src/journey.mjs +97 -0
- package/src/levels.mjs +145 -0
- package/src/local-agent.mjs +184 -0
- package/src/main.jsx +2481 -0
- package/src/model-policy-preflight.mjs +34 -0
- package/src/openai-voice.mjs +102 -0
- package/src/output-bundle.mjs +13 -0
- package/src/ownership-inspection.mjs +28 -0
- package/src/pack.mjs +201 -0
- package/src/pricing.mjs +85 -0
- package/src/production-state.mjs +8 -0
- package/src/project-copy.mjs +10 -0
- package/src/project-file.mjs +160 -0
- package/src/project-links.mjs +3 -0
- package/src/protected-state.mjs +43 -0
- package/src/public-faq-builder.mjs +19 -0
- package/src/public-faq-clouds.mjs +251 -0
- package/src/public-faq.mjs +122 -0
- package/src/pulumi.mjs +123 -0
- package/src/python-container.mjs +25 -0
- package/src/python-runtime.mjs +50 -0
- package/src/rag-handler.mjs +43 -0
- package/src/rag-web-assets.mjs +52 -0
- package/src/rag-web.mjs +68 -0
- package/src/records.mjs +177 -0
- package/src/resources.mjs +163 -0
- package/src/review.mjs +252 -0
- package/src/runtime/EnterpriseDocument.java +89 -0
- package/src/runtime/approved_http_destination.py +72 -0
- package/src/runtime/aws-lambda-qualification-lock.json +7 -0
- package/src/runtime/aws-python-lock.json +82 -0
- package/src/runtime/aws-web-python-lock.json +887 -0
- package/src/runtime/aws_json_model.py +42 -0
- package/src/runtime/azure-python-lock.json +863 -0
- package/src/runtime/azure-sources.json +8 -0
- package/src/runtime/azure-test-lock.json +23 -0
- package/src/runtime/azure_acceptance.py +192 -0
- package/src/runtime/azure_structured.py +153 -0
- package/src/runtime/bounded_rag.py +126 -0
- package/src/runtime/catalog_admin.py +70 -0
- package/src/runtime/cloud-sql-proxy-lock.json +8 -0
- package/src/runtime/cloud-sql-proxy-security-review.json +30 -0
- package/src/runtime/connector_cli.py +50 -0
- package/src/runtime/connector_sync.py +70 -0
- package/src/runtime/durable-python-lock.json +247 -0
- package/src/runtime/durable_workflow.py +325 -0
- package/src/runtime/enterprise-application-manifest.json +110 -0
- package/src/runtime/enterprise-application-sources.json +266 -0
- package/src/runtime/enterprise-auth-python-lock.json +592 -0
- package/src/runtime/enterprise-aws-host-bootstrap-source.json +1 -0
- package/src/runtime/enterprise-aws-host-probe-source.json +1 -0
- package/src/runtime/enterprise-aws-task-template.json +235 -0
- package/src/runtime/enterprise-document-lock.json +18 -0
- package/src/runtime/enterprise-document.Dockerfile +15 -0
- package/src/runtime/enterprise-runtime-aws-python-lock.json +1115 -0
- package/src/runtime/enterprise-runtime-azure-python-lock.json +1106 -0
- package/src/runtime/enterprise-runtime-gcp-python-lock.json +1060 -0
- package/src/runtime/enterprise-runtime-ibm-python-lock.json +1096 -0
- package/src/runtime/enterprise-runtime-python-lock.json +1058 -0
- package/src/runtime/enterprise-runtime.Dockerfile +15 -0
- package/src/runtime/enterprise-web-app.js +1 -0
- package/src/runtime/enterprise-web-entry.js +105 -0
- package/src/runtime/enterprise-web-lock.json +29 -0
- package/src/runtime/enterprise-web-notices.txt +229 -0
- package/src/runtime/enterprise-web.css +4 -0
- package/src/runtime/enterprise-web.html +8 -0
- package/src/runtime/enterprise_audit.py +91 -0
- package/src/runtime/enterprise_audit_schema.sql +25 -0
- package/src/runtime/enterprise_auth_schema.sql +67 -0
- package/src/runtime/enterprise_auth_store.py +241 -0
- package/src/runtime/enterprise_aws_host_bootstrap.py +120 -0
- package/src/runtime/enterprise_aws_host_probe.py +105 -0
- package/src/runtime/enterprise_aws_hosts.py +136 -0
- package/src/runtime/enterprise_aws_jobs.py +300 -0
- package/src/runtime/enterprise_aws_objects.py +93 -0
- package/src/runtime/enterprise_aws_transfer.py +83 -0
- package/src/runtime/enterprise_azure_batch_api.py +128 -0
- package/src/runtime/enterprise_azure_host.py +599 -0
- package/src/runtime/enterprise_azure_jobs.py +294 -0
- package/src/runtime/enterprise_azure_objects.py +234 -0
- package/src/runtime/enterprise_azure_pool.py +161 -0
- package/src/runtime/enterprise_azure_transfer.py +122 -0
- package/src/runtime/enterprise_configuration.py +136 -0
- package/src/runtime/enterprise_document.py +116 -0
- package/src/runtime/enterprise_document_cli.py +32 -0
- package/src/runtime/enterprise_document_contract.py +54 -0
- package/src/runtime/enterprise_document_job.py +109 -0
- package/src/runtime/enterprise_document_jobs.py +122 -0
- package/src/runtime/enterprise_document_jobs_schema.sql +23 -0
- package/src/runtime/enterprise_extraction_cache.py +186 -0
- package/src/runtime/enterprise_extraction_schema.sql +17 -0
- package/src/runtime/enterprise_google_jobs.py +275 -0
- package/src/runtime/enterprise_google_transfer.py +85 -0
- package/src/runtime/enterprise_http.py +212 -0
- package/src/runtime/enterprise_identity.py +52 -0
- package/src/runtime/enterprise_knowledge.py +152 -0
- package/src/runtime/enterprise_models.py +94 -0
- package/src/runtime/enterprise_native.py +181 -0
- package/src/runtime/enterprise_oauth.py +132 -0
- package/src/runtime/enterprise_operator.py +160 -0
- package/src/runtime/enterprise_parser_service.py +136 -0
- package/src/runtime/enterprise_parser_transport.py +68 -0
- package/src/runtime/enterprise_queries.py +255 -0
- package/src/runtime/enterprise_queries_schema.sql +27 -0
- package/src/runtime/enterprise_query_service.py +111 -0
- package/src/runtime/enterprise_secrets.py +110 -0
- package/src/runtime/enterprise_sessions.py +95 -0
- package/src/runtime/enterprise_sources.py +260 -0
- package/src/runtime/enterprise_web.py +39 -0
- package/src/runtime/enterprise_worker.py +68 -0
- package/src/runtime/export_contract_acceptance.py +18 -0
- package/src/runtime/faq-output-contract.json +40 -0
- package/src/runtime/faq-web-app.js +41 -0
- package/src/runtime/faq-web.css +1 -0
- package/src/runtime/faq-web.html +6 -0
- package/src/runtime/gcp-sources.json +9 -0
- package/src/runtime/gcp_acceptance.py +110 -0
- package/src/runtime/gcp_structured.py +150 -0
- package/src/runtime/http_acceptance.py +39 -0
- package/src/runtime/iap-web-app.js +36 -0
- package/src/runtime/iap-web-source.json +1 -0
- package/src/runtime/ibm-python-lock.json +856 -0
- package/src/runtime/ibm-sources.json +9 -0
- package/src/runtime/ibm_acceptance.py +133 -0
- package/src/runtime/ibm_structured.py +113 -0
- package/src/runtime/invoice-azapi-provider-lock.json +7 -0
- package/src/runtime/invoice-ibm-provider-lock.json +7 -0
- package/src/runtime/invoice-parser-lock.json +1199 -0
- package/src/runtime/invoice-parser-python-lock.json +56 -0
- package/src/runtime/invoice-parser.Dockerfile +190 -0
- package/src/runtime/invoice-sources.json +58 -0
- package/src/runtime/invoice-test-sources.json +24 -0
- package/src/runtime/invoice-web.css +2 -0
- package/src/runtime/invoice-web.html +12 -0
- package/src/runtime/invoice-web.js +112 -0
- package/src/runtime/invoice_configuration.py +17 -0
- package/src/runtime/invoice_document.py +95 -0
- package/src/runtime/invoice_extraction.py +96 -0
- package/src/runtime/invoice_http.py +129 -0
- package/src/runtime/invoice_identity.py +5 -0
- package/src/runtime/invoice_maintenance.py +40 -0
- package/src/runtime/invoice_native.py +77 -0
- package/src/runtime/invoice_object_store.py +153 -0
- package/src/runtime/invoice_operations.py +108 -0
- package/src/runtime/invoice_operator.py +79 -0
- package/src/runtime/invoice_operator_core.py +39 -0
- package/src/runtime/invoice_operator_job.py +147 -0
- package/src/runtime/invoice_parser_cli.py +17 -0
- package/src/runtime/invoice_parser_identity.py +39 -0
- package/src/runtime/invoice_parser_service.py +89 -0
- package/src/runtime/invoice_parser_transport.py +89 -0
- package/src/runtime/invoice_processing.py +24 -0
- package/src/runtime/invoice_retention.py +16 -0
- package/src/runtime/invoice_runtime_secrets.py +43 -0
- package/src/runtime/invoice_stage.py +80 -0
- package/src/runtime/invoice_web.py +39 -0
- package/src/runtime/invoice_worker.py +50 -0
- package/src/runtime/lambda_faq.py +5 -0
- package/src/runtime/parser-requirements.lock +4 -0
- package/src/runtime/policy_review.py +18 -0
- package/src/runtime/postgres-qualification-lock.json +21 -0
- package/src/runtime/public-faq-sources.json +22 -0
- package/src/runtime/public_catalog_admin.py +122 -0
- package/src/runtime/public_catalog_store.py +125 -0
- package/src/runtime/public_faq.py +144 -0
- package/src/runtime/public_faq_app.py +49 -0
- package/src/runtime/python-container-lock.json +18 -0
- package/src/runtime/rag-sources.json +10 -0
- package/src/runtime/rag_acceptance.py +60 -0
- package/src/runtime/rag_aws_adapter.py +95 -0
- package/src/runtime/rds-ca-source.json +9 -0
- package/src/runtime/rds-global-ca.pem +2888 -0
- package/src/runtime/state-contract-check-source.json +4 -0
- package/src/runtime/state_contract_check.py +338 -0
- package/src/runtime/structured-source.json +1 -0
- package/src/runtime/structured-web-app.js +71 -0
- package/src/runtime/structured-web-source.json +1 -0
- package/src/runtime/structured_http.py +105 -0
- package/src/runtime/structured_output.py +48 -0
- package/src/runtime/web-python-lock.json +802 -0
- package/src/runtime/web_user_admin.py +44 -0
- package/src/runtime/workflow_application.py +62 -0
- package/src/runtime/workflow_bindings.py +128 -0
- package/src/runtime/workflow_configuration.py +40 -0
- package/src/runtime/workflow_connection.py +105 -0
- package/src/runtime/workflow_contract.py +54 -0
- package/src/runtime/workflow_database.py +94 -0
- package/src/runtime/workflow_errors.py +6 -0
- package/src/runtime/workflow_http.py +178 -0
- package/src/runtime/workflow_identity.py +69 -0
- package/src/runtime/workflow_payload.py +118 -0
- package/src/runtime/workflow_process_stage.py +88 -0
- package/src/runtime/workflow_profile.py +91 -0
- package/src/runtime/workflow_review.py +106 -0
- package/src/runtime/workflow_role_bootstrap.py +104 -0
- package/src/runtime/workflow_schema.sql +72 -0
- package/src/runtime/workflow_worker.py +142 -0
- package/src/runtime-settings.mjs +15 -0
- package/src/s3-access.mjs +14 -0
- package/src/source-contracts.mjs +17 -0
- package/src/spec-kit.mjs +186 -0
- package/src/state-custody.mjs +32 -0
- package/src/state-inspection.mjs +45 -0
- package/src/state-spec-kit.mjs +13 -0
- package/src/storage-tests.mjs +25 -0
- package/src/structured-api.mjs +133 -0
- package/src/structured-contract.mjs +19 -0
- package/src/structured-operation-policy.mjs +2 -0
- package/src/structured-web.mjs +17 -0
- package/src/style.css +1 -0
- package/src/system-description.mjs +26 -0
- package/src/terraform-admission.mjs +66 -0
- package/src/terraform-composition.mjs +83 -0
- package/src/terraform-expressions.mjs +46 -0
- package/src/terraform-validation-coverage.mjs +11 -0
- package/src/terraform.mjs +128 -0
- package/src/text-statistics.mjs +51 -0
- package/src/upstream/openarchflow/format-payload.ts +26 -0
- package/src/upstream/openarchflow/import/mermaid.ts +205 -0
- package/src/upstream/openarchflow/import/sql-ddl.ts +109 -0
- package/src/upstream/openarchflow/layout.ts +54 -0
- package/src/upstream/openarchflow/providers/aws.ts +196 -0
- package/src/upstream/openarchflow/providers/azure.ts +126 -0
- package/src/upstream/openarchflow/providers/cloudnative.ts +71 -0
- package/src/upstream/openarchflow/providers/generic.ts +227 -0
- package/src/upstream/openarchflow/providers/integration.ts +22 -0
- package/src/upstream/openarchflow/providers/types.ts +2 -0
- package/src/upstream/openarchflow/templates.ts +937 -0
- package/src/upstream-catalog.mjs +10 -0
- package/src/useChatScroll.jsx +37 -0
- package/src/useEmulation.jsx +36 -0
- package/src/useEngineeringStatus.mjs +17 -0
- package/src/validation-receipt.mjs +10 -0
- package/src/visual-export.mjs +92 -0
- package/src/voice-conversation.mjs +117 -0
- package/src/work-intake.mjs +24 -0
- package/src/workflow-runtime-inventory.mjs +3 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Owned SQL bootstrap/enrollment/revocation; never an HTTP user operation.
|
|
2
|
+
|
|
3
|
+
Existing company/source IDs come from the approved directory/IT handoff. Do not
|
|
4
|
+
self-activate a source mapping by matching email strings or unverified JWT claims.
|
|
5
|
+
No cloud identity, database, role, replacement key or source registration is created.
|
|
6
|
+
"""
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from psycopg.rows import dict_row
|
|
14
|
+
import psycopg
|
|
15
|
+
from enterprise_configuration import deployment,read_deployment,native_database,session_domain
|
|
16
|
+
from enterprise_auth_store import initialize_auth_store,grant_auth_runtime
|
|
17
|
+
from enterprise_audit import initialize_audit,grant_audit_writer
|
|
18
|
+
from enterprise_extraction_cache import initialize_extraction_cache,grant_extraction_runtime
|
|
19
|
+
from enterprise_queries import initialize_query_store,grant_query_runtime
|
|
20
|
+
from enterprise_document_jobs import initialize_document_jobs,grant_document_jobs_runtime
|
|
21
|
+
from enterprise_secrets import SecretFiles
|
|
22
|
+
from public_faq import object_pairs
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def initialize(conn,api_role,worker_role):
|
|
26
|
+
if conn.autocommit or not isinstance(api_role,str)or not isinstance(worker_role,str)or api_role==worker_role:
|
|
27
|
+
raise ValueError('Use an operator transaction and separate existing API/worker SQL identities')
|
|
28
|
+
with conn.transaction():
|
|
29
|
+
existing=conn.execute("SELECT count(*)FROM pg_namespace WHERE nspname IN('enterprise_auth','enterprise_queries','enterprise_extractions','enterprise_audit','enterprise_document_jobs')").fetchone()[0]
|
|
30
|
+
if existing:raise ValueError('Private schemas already exist; no adoption or implicit migration is permitted')
|
|
31
|
+
initialize_auth_store(conn);initialize_query_store(conn);initialize_extraction_cache(conn);initialize_audit(conn);initialize_document_jobs(conn)
|
|
32
|
+
grant_auth_runtime(conn,api_role);grant_query_runtime(conn,api_role);grant_audit_writer(conn,api_role)
|
|
33
|
+
grant_auth_runtime(conn,worker_role);grant_query_runtime(conn,worker_role,worker=True)
|
|
34
|
+
grant_extraction_runtime(conn,worker_role);grant_audit_writer(conn,worker_role);grant_document_jobs_runtime(conn,worker_role)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def checked_identity(metadata,subject):
|
|
38
|
+
owned=deployment(metadata)
|
|
39
|
+
if not isinstance(subject,str)or not 1<=len(subject.encode('utf-8'))<=255 or any(ord(c)<32 for c in subject):
|
|
40
|
+
raise ValueError('Supply the existing verified company API subject from the approved directory handoff')
|
|
41
|
+
key=hashlib.sha256((owned['company']['issuer']+'\0'+subject).encode()).hexdigest()
|
|
42
|
+
return owned,key
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def require_schema(cursor):
|
|
46
|
+
for schema,filename in [('enterprise_auth','enterprise_auth_schema.sql'),('enterprise_queries','enterprise_queries_schema.sql')]:
|
|
47
|
+
observed=cursor.execute('SELECT version,source_sha256 FROM '+schema+'.schema_version').fetchone()
|
|
48
|
+
expected=hashlib.sha256(Path(__file__).with_name(filename).read_bytes()).hexdigest()
|
|
49
|
+
if observed!={'version':1,'source_sha256':expected}:raise ValueError('Operator schema differs; use the reviewed lifecycle')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def journal(cursor,account,key,action,active,alias=None,version=None,registration_version=None,domain=None):
|
|
53
|
+
cursor.execute('INSERT INTO enterprise_auth.administration_events(account_id,principal_key,action,active,source_alias,mapping_version,registration_version,registration_domain)VALUES(%s,%s,%s,%s,%s,%s,%s,%s)',(account,key,action,active,alias,version,registration_version,domain))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cancel_requests(cursor,account,key=None):
|
|
57
|
+
suffix=' AND principal_key=%s'if key is not None else''
|
|
58
|
+
rows=cursor.execute("UPDATE enterprise_queries.requests SET state='cancelled',question_ciphertext=NULL,result_ciphertext=NULL,lease_token=NULL,lease_until=NULL,error_code=NULL,updated_at=clock_timestamp()WHERE account_id=%s AND state IN('queued','running','ready')"+suffix+' RETURNING id,principal_key,attempt',(account,key)if key is not None else(account,)).fetchall()
|
|
59
|
+
for row in rows:cursor.execute("INSERT INTO enterprise_queries.events(query_id,account_id,principal_key,event,attempt)VALUES(%s,%s,%s,'cancelled',%s)",(row['id'],account,row['principal_key'],row['attempt']))
|
|
60
|
+
return rows
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def activate(conn,metadata):
|
|
64
|
+
owned=deployment(metadata);account=owned['accountId'];domain=session_domain(owned)
|
|
65
|
+
if conn.autocommit:raise ValueError('Use an operator transaction')
|
|
66
|
+
with conn.transaction(),conn.cursor(row_factory=dict_row)as cursor:
|
|
67
|
+
require_schema(cursor)
|
|
68
|
+
cursor.execute('SELECT pg_advisory_xact_lock(hashtextextended(%s,0))',('enterprise-company:'+account,))
|
|
69
|
+
row=cursor.execute('SELECT session_domain,version FROM enterprise_auth.company_bindings WHERE account_id=%s FOR UPDATE',(account,)).fetchone()
|
|
70
|
+
if row and row['session_domain']==domain:return {'changed':False,'registrationVersion':row['version']}
|
|
71
|
+
version=row['version']+1 if row else 1
|
|
72
|
+
cursor.execute('INSERT INTO enterprise_auth.company_bindings(account_id,session_domain,version)VALUES(%s,%s,%s)ON CONFLICT(account_id)DO UPDATE SET session_domain=EXCLUDED.session_domain,version=EXCLUDED.version,actor=current_user,updated_at=clock_timestamp()',(account,domain,version))
|
|
73
|
+
cursor.execute('UPDATE enterprise_auth.sessions SET revoked_at=clock_timestamp()WHERE account_id=%s AND revoked_at IS NULL',(account,))
|
|
74
|
+
cursor.execute('DELETE FROM enterprise_auth.attempts WHERE account_id=%s',(account,))
|
|
75
|
+
cancel_requests(cursor,account)
|
|
76
|
+
journal(cursor,account,None,'company_registration_updated',True,registration_version=version,domain=domain)
|
|
77
|
+
return {'changed':True,'registrationVersion':version}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def require_company(cursor,owned):
|
|
81
|
+
row=cursor.execute('SELECT session_domain FROM enterprise_auth.company_bindings WHERE account_id=%s FOR SHARE',(owned['accountId'],)).fetchone()
|
|
82
|
+
if not row or row['session_domain']!=session_domain(owned):raise ValueError('Activate the owned company registration before changing directory membership')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def reset_source(cursor,account,key,source):
|
|
86
|
+
cursor.execute("UPDATE enterprise_auth.connections SET credential_ciphertext=NULL,status='revoked',revision=revision+1,updated_at=clock_timestamp()WHERE account_id=%s AND principal_key=%s AND source_id=%s",(account,key,source))
|
|
87
|
+
cursor.execute('DELETE FROM enterprise_auth.attempts WHERE account_id=%s AND principal_key=%s AND source_id=%s',(account,key,source))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def enroll(conn,metadata,subject,mappings):
|
|
91
|
+
owned,key=checked_identity(metadata,subject)
|
|
92
|
+
if conn.autocommit or not isinstance(mappings,dict)or set(mappings)!={'sharepoint','confluence'}or any(not isinstance(v,str)or not 1<=len(v.encode('utf-8'))<=255 or any(ord(c)<32 for c in v)for v in mappings.values()):
|
|
93
|
+
raise ValueError('Supply both approved native source-account mappings in an operator transaction')
|
|
94
|
+
account=owned['accountId'];sources={'sharepoint':'sharepoint:'+owned['sources']['sharepoint']['driveId'],'confluence':'confluence:'+owned['sources']['confluence']['cloudId']}
|
|
95
|
+
with conn.transaction(),conn.cursor(row_factory=dict_row)as cursor:
|
|
96
|
+
require_schema(cursor)
|
|
97
|
+
require_company(cursor,owned)
|
|
98
|
+
cursor.execute('SELECT pg_advisory_xact_lock(hashtextextended(%s,0))',('enterprise-provision:'+account+':'+key,))
|
|
99
|
+
current=cursor.execute('SELECT issuer,subject,active FROM enterprise_auth.memberships WHERE account_id=%s AND principal_key=%s FOR UPDATE',(account,key)).fetchone()
|
|
100
|
+
if current and(current['issuer']!=owned['company']['issuer']or current['subject']!=subject):raise ValueError('Existing company identity binding differs')
|
|
101
|
+
if not current or not current['active']:
|
|
102
|
+
cursor.execute('INSERT INTO enterprise_auth.memberships(account_id,principal_key,issuer,subject,active)VALUES(%s,%s,%s,%s,true)ON CONFLICT(account_id,principal_key)DO UPDATE SET active=true',(account,key,owned['company']['issuer'],subject))
|
|
103
|
+
journal(cursor,account,key,'membership_enabled',True)
|
|
104
|
+
changed=0
|
|
105
|
+
for alias,source in sources.items():
|
|
106
|
+
old=cursor.execute('SELECT source_subject,version,active FROM enterprise_auth.source_mappings WHERE account_id=%s AND principal_key=%s AND source_id=%s FOR UPDATE',(account,key,source)).fetchone()
|
|
107
|
+
if old and old['active']and old['source_subject']==mappings[alias]:continue
|
|
108
|
+
version=old['version']+1 if old else 1
|
|
109
|
+
cursor.execute('INSERT INTO enterprise_auth.source_mappings(account_id,principal_key,source_id,source_subject,version,active)VALUES(%s,%s,%s,%s,%s,true)ON CONFLICT(account_id,principal_key,source_id)DO UPDATE SET source_subject=EXCLUDED.source_subject,version=EXCLUDED.version,active=true',(account,key,source,mappings[alias],version))
|
|
110
|
+
reset_source(cursor,account,key,source)
|
|
111
|
+
journal(cursor,account,key,'mapping_updated',True,alias,version);changed+=1
|
|
112
|
+
return {'principalKey':key,'active':True,'mappingsChanged':changed}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def revoke(conn,metadata,subject):
|
|
116
|
+
owned,key=checked_identity(metadata,subject)
|
|
117
|
+
if conn.autocommit:raise ValueError('Use an operator transaction')
|
|
118
|
+
account=owned['accountId']
|
|
119
|
+
with conn.transaction(),conn.cursor(row_factory=dict_row)as cursor:
|
|
120
|
+
require_schema(cursor)
|
|
121
|
+
require_company(cursor,owned)
|
|
122
|
+
cursor.execute('SELECT pg_advisory_xact_lock(hashtextextended(%s,0))',('enterprise-provision:'+account+':'+key,))
|
|
123
|
+
member=cursor.execute('SELECT active FROM enterprise_auth.memberships WHERE account_id=%s AND principal_key=%s FOR UPDATE',(account,key)).fetchone()
|
|
124
|
+
if not member:return {'principalKey':key,'active':False,'changed':False}
|
|
125
|
+
cursor.execute('UPDATE enterprise_auth.memberships SET active=false WHERE account_id=%s AND principal_key=%s',(account,key))
|
|
126
|
+
mappings=cursor.execute('UPDATE enterprise_auth.source_mappings SET active=false,version=version+1 WHERE account_id=%s AND principal_key=%s AND active RETURNING source_id,version',(account,key)).fetchall()
|
|
127
|
+
sources={'sharepoint:'+owned['sources']['sharepoint']['driveId']:'sharepoint','confluence:'+owned['sources']['confluence']['cloudId']:'confluence'}
|
|
128
|
+
for row in mappings:
|
|
129
|
+
reset_source(cursor,account,key,row['source_id'])
|
|
130
|
+
if row['source_id']in sources:journal(cursor,account,key,'mapping_updated',False,sources[row['source_id']],row['version'])
|
|
131
|
+
# Also erase any older/inactive registration's remaining credential.
|
|
132
|
+
cursor.execute("UPDATE enterprise_auth.connections SET credential_ciphertext=NULL,status='revoked',revision=revision+1,updated_at=clock_timestamp()WHERE account_id=%s AND principal_key=%s AND(status<>'revoked'OR credential_ciphertext IS NOT NULL)",(account,key))
|
|
133
|
+
cursor.execute('UPDATE enterprise_auth.sessions SET revoked_at=clock_timestamp()WHERE account_id=%s AND principal_key=%s AND revoked_at IS NULL',(account,key))
|
|
134
|
+
cursor.execute('DELETE FROM enterprise_auth.attempts WHERE account_id=%s AND principal_key=%s',(account,key))
|
|
135
|
+
requests=cancel_requests(cursor,account,key)
|
|
136
|
+
if member['active']:journal(cursor,account,key,'membership_disabled',False)
|
|
137
|
+
return {'principalKey':key,'active':False,'changed':bool(member['active']or mappings or requests)}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main():
|
|
141
|
+
if sys.argv[1:]:raise SystemExit('Supply the owned operator command as private JSON on stdin')
|
|
142
|
+
raw=sys.stdin.buffer.read(32769)
|
|
143
|
+
if len(raw)>32768:raise ValueError('Operator command exceeds its boundary')
|
|
144
|
+
command=json.loads(raw,object_pairs_hook=object_pairs)
|
|
145
|
+
if not isinstance(command,dict):raise ValueError('Use an owned operator command')
|
|
146
|
+
mode=command.get('operation')
|
|
147
|
+
expected={'initialize':{'operation','apiRole','workerRole'},'activate':{'operation'},'enroll':{'operation','subject','mappings'},'revoke':{'operation','subject'}}
|
|
148
|
+
if mode not in expected or set(command)!=expected[mode]:raise ValueError('Invalid owned operator command')
|
|
149
|
+
owned=read_deployment()
|
|
150
|
+
with SecretFiles(owned['provider'],os.environ,'operator')as secrets:
|
|
151
|
+
database=native_database(owned['provider'],secrets.env,'migration',secrets)
|
|
152
|
+
with psycopg.connect(database(),connect_timeout=5)as conn,conn.transaction():
|
|
153
|
+
if mode=='initialize':initialize(conn,command['apiRole'],command['workerRole']);activate(conn,owned);result={'initialized':True}
|
|
154
|
+
elif mode=='activate':result=activate(conn,owned)
|
|
155
|
+
elif mode=='enroll':result=enroll(conn,owned,command['subject'],command['mappings'])
|
|
156
|
+
else:result=revoke(conn,owned,command['subject'])
|
|
157
|
+
print(json.dumps(result))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__=='__main__':main()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Private byte-only company-document RPC; credentialless parser workload.
|
|
2
|
+
|
|
3
|
+
Network admission, TLS and egress isolation are mandatory infrastructure controls.
|
|
4
|
+
The service accepts no URL, model input, parser option or company/source identity.
|
|
5
|
+
"""
|
|
6
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import socket
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
15
|
+
from enterprise_document import ExtractionError, MAX_INPUT, media_type
|
|
16
|
+
from enterprise_document_cli import installed_parser
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DocumentServer(ThreadingHTTPServer):
|
|
20
|
+
daemon_threads = True
|
|
21
|
+
request_queue_size = 16
|
|
22
|
+
|
|
23
|
+
def __init__(self, address, parser, authorize):
|
|
24
|
+
if not callable(getattr(parser, 'extract', None)) or not callable(authorize):
|
|
25
|
+
raise ValueError('Supply the owned parser and explicit caller admission adapter')
|
|
26
|
+
self.parser, self.authorize = parser, authorize
|
|
27
|
+
self.slot = threading.BoundedSemaphore(1)
|
|
28
|
+
super().__init__(address, DocumentHandler)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DocumentHandler(BaseHTTPRequestHandler):
|
|
32
|
+
protocol_version = 'HTTP/1.0'
|
|
33
|
+
|
|
34
|
+
def log_message(self, *args):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def setup(self):
|
|
38
|
+
super().setup()
|
|
39
|
+
self.connection.settimeout(5)
|
|
40
|
+
|
|
41
|
+
def reply(self, status, value):
|
|
42
|
+
body = json.dumps(value, ensure_ascii=False, allow_nan=False).encode()
|
|
43
|
+
self.send_response(status)
|
|
44
|
+
for key, value in [('Content-Type', 'application/json'), ('Content-Length', str(len(body))),
|
|
45
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff')]:
|
|
46
|
+
self.send_header(key, value)
|
|
47
|
+
self.end_headers()
|
|
48
|
+
self.wfile.write(body)
|
|
49
|
+
|
|
50
|
+
def do_GET(self):
|
|
51
|
+
self.reply(200, {'status': 'running'}) if self.path == '/healthz' else self.reply(404, {'status': 'rejected'})
|
|
52
|
+
|
|
53
|
+
def do_POST(self):
|
|
54
|
+
if self.path != '/extract':
|
|
55
|
+
self.reply(404, {'status': 'rejected'})
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
self.server.authorize(self.headers.get_all('Authorization') or [])
|
|
59
|
+
except Exception:
|
|
60
|
+
self.reply(403, {'status': 'rejected', 'reason': 'access_denied'})
|
|
61
|
+
return
|
|
62
|
+
lengths = self.headers.get_all('Content-Length') or []
|
|
63
|
+
types = self.headers.get_all('Content-Type') or []
|
|
64
|
+
if self.headers.get('Transfer-Encoding') or len(lengths) != 1 or not lengths[0].isascii() or not lengths[0].isdigit():
|
|
65
|
+
self.reply(411, {'status': 'rejected', 'reason': 'invalid_request'})
|
|
66
|
+
return
|
|
67
|
+
if len(lengths[0]) > 10 or not 1 <= int(lengths[0]) <= MAX_INPUT:
|
|
68
|
+
self.reply(413, {'status': 'rejected', 'reason': 'document_too_large'})
|
|
69
|
+
return
|
|
70
|
+
try:
|
|
71
|
+
if len(types) != 1:
|
|
72
|
+
raise ExtractionError('invalid_document')
|
|
73
|
+
declared = media_type(types[0])
|
|
74
|
+
except ExtractionError:
|
|
75
|
+
self.reply(415, {'status': 'rejected', 'reason': 'invalid_document'})
|
|
76
|
+
return
|
|
77
|
+
if not self.server.slot.acquire(blocking=False):
|
|
78
|
+
self.reply(429, {'status': 'rejected', 'reason': 'busy'})
|
|
79
|
+
return
|
|
80
|
+
try:
|
|
81
|
+
remaining, body, deadline = int(lengths[0]), bytearray(), time.monotonic() + 15
|
|
82
|
+
while remaining:
|
|
83
|
+
left = deadline - time.monotonic()
|
|
84
|
+
if left <= 0:
|
|
85
|
+
raise TimeoutError()
|
|
86
|
+
self.connection.settimeout(min(5, left))
|
|
87
|
+
chunk = self.rfile.read1(min(remaining, 65536))
|
|
88
|
+
if not chunk:
|
|
89
|
+
raise ValueError()
|
|
90
|
+
body.extend(chunk)
|
|
91
|
+
remaining -= len(chunk)
|
|
92
|
+
self.connection.settimeout(5)
|
|
93
|
+
document = self.server.parser.extract(bytes(body), declared)
|
|
94
|
+
self.reply(200, {'status': 'ok', 'document': document})
|
|
95
|
+
except ExtractionError as failure:
|
|
96
|
+
self.reply(422, {'status': 'rejected', 'reason': failure.code})
|
|
97
|
+
except (TimeoutError, socket.timeout):
|
|
98
|
+
self.reply(408, {'status': 'rejected', 'reason': 'upload_timeout'})
|
|
99
|
+
except ValueError:
|
|
100
|
+
self.reply(400, {'status': 'rejected', 'reason': 'invalid_request'})
|
|
101
|
+
except Exception:
|
|
102
|
+
self.reply(503, {'status': 'rejected', 'reason': 'extraction_unavailable'})
|
|
103
|
+
finally:
|
|
104
|
+
self.server.slot.release()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main():
|
|
108
|
+
forbidden = ['DATABASE_PASSWORD', 'DATABASE_PASSWORD_FILE', 'DATABASE_HOST', 'DATABASE_CONNECTION_NAME',
|
|
109
|
+
'DESTINATION_TOKEN_FILE', 'DESTINATION_ENDPOINT', 'GOOGLE_APPLICATION_CREDENTIALS', 'AWS_ACCESS_KEY_ID',
|
|
110
|
+
'AWS_SECRET_ACCESS_KEY', 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI', 'AWS_CONTAINER_CREDENTIALS_FULL_URI',
|
|
111
|
+
'IDENTITY_ENDPOINT', 'IDENTITY_HEADER', 'MSI_ENDPOINT', 'MSI_SECRET', 'MSAL_CLIENT_SECRET', 'CONFLUENCE_CLIENT_SECRET']
|
|
112
|
+
if any(os.environ.get(key) for key in forbidden) or Path('/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token').exists():
|
|
113
|
+
raise ValueError('Do not supply source, database, model or workload credentials to the parser')
|
|
114
|
+
mode = os.environ.get('PARSER_AUTH_MODE', '')
|
|
115
|
+
if mode == 'shared-token':
|
|
116
|
+
from invoice_parser_identity import SharedParserVerifier
|
|
117
|
+
verify = SharedParserVerifier(os.environ.pop('PARSER_SHARED_TOKEN', ''))
|
|
118
|
+
def authorize(headers):
|
|
119
|
+
if len(headers) != 1 or not headers[0].startswith('Bearer '):
|
|
120
|
+
raise PermissionError()
|
|
121
|
+
verify(headers[0][7:])
|
|
122
|
+
elif mode == 'private-ingress':
|
|
123
|
+
# Explicitly selected only when the native hosting boundary authenticates
|
|
124
|
+
# and restricts callers. An arbitrary request header is never that proof.
|
|
125
|
+
def authorize(headers):
|
|
126
|
+
return None
|
|
127
|
+
else:
|
|
128
|
+
raise ValueError('Select explicit owned parser admission')
|
|
129
|
+
port = int(os.environ.get('PORT', '8080'))
|
|
130
|
+
if not 1024 <= port <= 65535:
|
|
131
|
+
raise ValueError('Use an owned nonprivileged parser port')
|
|
132
|
+
DocumentServer(('0.0.0.0', port), installed_parser(), authorize).serve_forever(poll_interval=.2)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == '__main__':
|
|
136
|
+
main()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Application-side fixed private HTTPS document parser adapter."""
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from urllib.parse import urlsplit
|
|
5
|
+
import requests
|
|
6
|
+
from enterprise_document_contract import MAX_INPUT, document_result, media_type
|
|
7
|
+
from enterprise_sources import SourceUnavailable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def unique_object(pairs):
|
|
11
|
+
result = {}
|
|
12
|
+
for key, value in pairs:
|
|
13
|
+
if key in result:
|
|
14
|
+
raise ValueError('Duplicate response field')
|
|
15
|
+
result[key] = value
|
|
16
|
+
return result
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DocumentClient:
|
|
20
|
+
def __init__(self, endpoint, authorization, *, session=None):
|
|
21
|
+
try:
|
|
22
|
+
target = urlsplit(endpoint)
|
|
23
|
+
if (not isinstance(endpoint, str) or len(endpoint) > 2000 or target.scheme != 'https'
|
|
24
|
+
or not target.hostname or target.path != '/extract' or target.query or target.fragment
|
|
25
|
+
or target.username or target.password or (target.port is not None and not 1 <= target.port <= 65535)
|
|
26
|
+
or any(ord(c) < 33 or ord(c) > 126 for c in endpoint)):
|
|
27
|
+
raise ValueError()
|
|
28
|
+
except (TypeError, ValueError):
|
|
29
|
+
raise ValueError('Supply the fixed private HTTPS /extract endpoint') from None
|
|
30
|
+
if authorization is not None and not callable(authorization):
|
|
31
|
+
raise ValueError('Supply an owned workload admission adapter')
|
|
32
|
+
self.endpoint, self.authorization = endpoint, authorization
|
|
33
|
+
# No shared session mutation during parallel source extraction.
|
|
34
|
+
self.session = session
|
|
35
|
+
|
|
36
|
+
def __call__(self, raw, declared):
|
|
37
|
+
if not isinstance(raw, bytes) or not 0 < len(raw) <= MAX_INPUT:
|
|
38
|
+
raise SourceUnavailable('source_document_unsupported')
|
|
39
|
+
session = self.session or requests.Session()
|
|
40
|
+
session.trust_env = False
|
|
41
|
+
try:
|
|
42
|
+
headers = {'Content-Type': media_type(declared)}
|
|
43
|
+
if self.authorization is not None:
|
|
44
|
+
token = self.authorization()
|
|
45
|
+
if not isinstance(token, str) or not token.startswith('Bearer ') or not 8 <= len(token) <= 20000 or any(ord(c) < 33 or ord(c) > 126 for c in token[7:]):
|
|
46
|
+
raise ValueError('Invalid caller admission')
|
|
47
|
+
headers['Authorization'] = token
|
|
48
|
+
deadline = time.monotonic() + 125
|
|
49
|
+
with session.post(self.endpoint, data=raw, headers=headers, timeout=(2, 115), allow_redirects=False, stream=True) as response:
|
|
50
|
+
if response.status_code != 200:
|
|
51
|
+
raise SourceUnavailable('document_extraction_unavailable')
|
|
52
|
+
content = bytearray()
|
|
53
|
+
for chunk in response.iter_content(1):
|
|
54
|
+
content.extend(chunk)
|
|
55
|
+
if len(content) > 1201000 or time.monotonic() > deadline:
|
|
56
|
+
raise ValueError('Unconfirmed extraction boundary')
|
|
57
|
+
value = json.loads(content, object_pairs_hook=unique_object,
|
|
58
|
+
parse_constant=lambda _: (_ for _ in ()).throw(ValueError('Nonfinite output')))
|
|
59
|
+
if not isinstance(value, dict) or set(value) != {'status', 'document'} or value['status'] != 'ok':
|
|
60
|
+
raise ValueError('Unconfirmed extraction result')
|
|
61
|
+
return document_result(value['document'], raw, declared)['text']
|
|
62
|
+
except SourceUnavailable:
|
|
63
|
+
raise
|
|
64
|
+
except Exception:
|
|
65
|
+
raise SourceUnavailable('document_extraction_unavailable') from None
|
|
66
|
+
finally:
|
|
67
|
+
if self.session is None:
|
|
68
|
+
session.close()
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Encrypted durable question jobs; PostgreSQL/Procrastinate, no private queue args.
|
|
2
|
+
|
|
3
|
+
Raw browser cookies, JWTs, source credentials, questions and answers never enter
|
|
4
|
+
queue arguments. Domain leases independently fence duplicate/stale workers. A
|
|
5
|
+
ready result is only internal evidence until current native sources reauthorize it.
|
|
6
|
+
"""
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from dataclasses import dataclass,field
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import re
|
|
13
|
+
import uuid
|
|
14
|
+
import procrastinate
|
|
15
|
+
from procrastinate.schema import SchemaManager
|
|
16
|
+
from psycopg import sql
|
|
17
|
+
from enterprise_identity import CompanyPrincipal
|
|
18
|
+
from enterprise_sessions import CompanySessions
|
|
19
|
+
from enterprise_sources import query_text,SourceDenied,SourceUnavailable
|
|
20
|
+
|
|
21
|
+
TASK='enterprise.answer.v1'
|
|
22
|
+
QUEUE='enterprise-answer'
|
|
23
|
+
|
|
24
|
+
def task_identity(account_id):
|
|
25
|
+
if not isinstance(account_id,str)or not re.fullmatch('[A-Za-z0-9_-]{1,128}',account_id):raise ValueError('Supply the owned company account')
|
|
26
|
+
suffix=hashlib.sha256(account_id.encode()).hexdigest()[:16]
|
|
27
|
+
return TASK+'.'+suffix,QUEUE+'-'+suffix
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class QueryMissing(LookupError):pass
|
|
31
|
+
class QueryConflict(ValueError):pass
|
|
32
|
+
class QueryLeaseLost(SourceUnavailable):pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def identifier(value):
|
|
36
|
+
if not isinstance(value,str)or str(uuid.UUID(value))!=value:raise ValueError('Use a canonical request identity')
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class QueryLease:
|
|
42
|
+
id: str
|
|
43
|
+
token: str=field(repr=False)
|
|
44
|
+
principal: CompanyPrincipal=field(repr=False)
|
|
45
|
+
session_id: str=field(repr=False)
|
|
46
|
+
question: str=field(repr=False)
|
|
47
|
+
attempt: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EnterpriseQueryStore:
|
|
51
|
+
def __init__(self,auth,task,account_id,*,lease_seconds=120,ttl_seconds=900,session_domain=None):
|
|
52
|
+
name,queue=task_identity(account_id)
|
|
53
|
+
if task.name!=name or task.queue!=queue or getattr(task,'queueing_lock',None)is not None:raise ValueError('Use the owned query task without a queueing lock')
|
|
54
|
+
if type(lease_seconds)is not int or not 15<=lease_seconds<=600 or type(ttl_seconds)is not int or not lease_seconds<=ttl_seconds<=3600:raise ValueError('Invalid owned query deadlines')
|
|
55
|
+
self.auth=auth;self.task=task;self.account_id=account_id;self.lease_seconds=lease_seconds;self.ttl_seconds=ttl_seconds;self.sessions=CompanySessions(auth,domain=session_domain)
|
|
56
|
+
|
|
57
|
+
@contextmanager
|
|
58
|
+
def transaction(self):
|
|
59
|
+
with self.auth.transaction()as cur:
|
|
60
|
+
cur.execute('SET LOCAL search_path=pg_catalog,enterprise_queries,pg_temp')
|
|
61
|
+
yield cur
|
|
62
|
+
|
|
63
|
+
def context(self,row,purpose):
|
|
64
|
+
result=['knowledge-query',purpose,row['account_id'],row['principal_key'],row['session_sha256'],str(row['id'])]
|
|
65
|
+
if purpose=='answer':result.append(row['attempt'])
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
def _principal(self,cur,session_id):
|
|
69
|
+
if not isinstance(session_id,str)or not re.fullmatch('[0-9a-f]{64}',session_id):raise SourceDenied('company_identity_required')
|
|
70
|
+
row=cur.execute('SELECT s.account_id,s.principal_key,s.nonce_ciphertext,s.csrf_sha256,extract(epoch FROM s.expires_at)::bigint AS expires_at,m.issuer,m.subject FROM enterprise_auth.sessions s JOIN enterprise_auth.memberships m USING(account_id,principal_key)WHERE s.session_sha256=%s AND s.revoked_at IS NULL AND s.expires_at>clock_timestamp()',(session_id,)).fetchone()
|
|
71
|
+
if not row:raise SourceDenied('company_identity_required')
|
|
72
|
+
principal=CompanyPrincipal(row['account_id'],row['issuer'],row['subject'],row['expires_at'])
|
|
73
|
+
if principal.key!=row['principal_key']or principal.account_id!=self.account_id:raise SourceDenied('company_identity_required')
|
|
74
|
+
self.auth._authorize(cur,principal)
|
|
75
|
+
self.sessions.private(row,session_id)
|
|
76
|
+
return principal
|
|
77
|
+
|
|
78
|
+
def _owner(self,cur,session):
|
|
79
|
+
principal=self._principal(cur,session.id)
|
|
80
|
+
if principal.account_id!=session.principal.account_id or principal.key!=session.principal.key:raise SourceDenied('company_identity_required')
|
|
81
|
+
return principal
|
|
82
|
+
|
|
83
|
+
def _row(self,cur,identity):
|
|
84
|
+
identifier(identity)
|
|
85
|
+
return cur.execute('SELECT *,expires_at>clock_timestamp()AS current,lease_until>clock_timestamp()AS leased FROM enterprise_queries.requests WHERE id=%s AND account_id=%s FOR UPDATE',(identity,self.account_id)).fetchone()
|
|
86
|
+
|
|
87
|
+
def _event(self,cur,row,event):
|
|
88
|
+
cur.execute('INSERT INTO enterprise_queries.events(query_id,account_id,principal_key,event,attempt)VALUES(%s,%s,%s,%s,%s)',(row['id'],row['account_id'],row['principal_key'],event,row['attempt']))
|
|
89
|
+
|
|
90
|
+
def _end(self,cur,row,state,error=None):
|
|
91
|
+
cur.execute('UPDATE enterprise_queries.requests SET state=%s,error_code=%s,question_ciphertext=NULL,result_ciphertext=NULL,lease_token=NULL,lease_until=NULL,updated_at=clock_timestamp()WHERE id=%s',(state,error,row['id']))
|
|
92
|
+
self._event(cur,row,state)
|
|
93
|
+
|
|
94
|
+
def _enqueue(self,cur,identity,*,delay=0):
|
|
95
|
+
# No execution lock: domain leases fence duplicate notifications and allow
|
|
96
|
+
# a replacement to recover an expired lease while an old worker winds down.
|
|
97
|
+
scheduled=None
|
|
98
|
+
if delay:
|
|
99
|
+
scheduled=cur.execute('SELECT clock_timestamp()+make_interval(secs=>%s)AS at',(delay,)).fetchone()['at']
|
|
100
|
+
self.task.configure(connection=cur.connection,schedule_at=scheduled).defer(query_id=str(identity))
|
|
101
|
+
|
|
102
|
+
def submit(self,session,question,request_key):
|
|
103
|
+
question=query_text(question);identifier(request_key)
|
|
104
|
+
with self.transaction()as cur:
|
|
105
|
+
principal=self._owner(cur,session)
|
|
106
|
+
digest=self.auth.box.fingerprint(['question',principal.account_id,principal.key,session.id,request_key],question)
|
|
107
|
+
cur.execute('SELECT pg_advisory_xact_lock(hashtextextended(%s,0))',('knowledge-query:'+principal.account_id+':'+principal.key,))
|
|
108
|
+
existing=cur.execute('SELECT * FROM enterprise_queries.requests WHERE account_id=%s AND principal_key=%s AND session_sha256=%s AND request_key=%s FOR UPDATE',(principal.account_id,principal.key,session.id,request_key)).fetchone()
|
|
109
|
+
if existing:
|
|
110
|
+
if existing['question_mac']!=digest:raise QueryConflict('Request identity already refers to another question')
|
|
111
|
+
return str(existing['id'])
|
|
112
|
+
count=cur.execute("SELECT count(*)AS n FROM enterprise_queries.requests WHERE account_id=%s AND principal_key=%s AND state IN('queued','running')AND expires_at>clock_timestamp()",(principal.account_id,principal.key)).fetchone()['n']
|
|
113
|
+
if count>=3:raise SourceUnavailable('query_capacity_reached')
|
|
114
|
+
row={'id':str(uuid.uuid4()),'account_id':principal.account_id,'principal_key':principal.key,'session_sha256':session.id,'attempt':0}
|
|
115
|
+
encrypted=self.auth.box.seal(self.context(row,'question'),{'question':question})
|
|
116
|
+
cur.execute("INSERT INTO enterprise_queries.requests(id,account_id,principal_key,session_sha256,request_key,question_mac,question_ciphertext,state,expires_at)VALUES(%s,%s,%s,%s,%s,%s,%s,'queued',least(to_timestamp(%s),clock_timestamp()+make_interval(secs=>%s)))",(row['id'],principal.account_id,principal.key,session.id,request_key,digest,encrypted,principal.expires_at,self.ttl_seconds))
|
|
117
|
+
self._enqueue(cur,row['id']);self._event(cur,row,'submitted')
|
|
118
|
+
return row['id']
|
|
119
|
+
|
|
120
|
+
def get(self,session,identity):
|
|
121
|
+
with self.transaction()as cur:
|
|
122
|
+
principal=self._owner(cur,session);row=self._row(cur,identity)
|
|
123
|
+
if not row or row['account_id']!=principal.account_id or row['principal_key']!=principal.key or row['session_sha256']!=session.id:raise QueryMissing('Question unavailable')
|
|
124
|
+
if not row['current']and row['state']!='expired':
|
|
125
|
+
self._end(cur,row,'expired');row.update(state='expired',question_ciphertext=None,result_ciphertext=None)
|
|
126
|
+
return row
|
|
127
|
+
|
|
128
|
+
def cancel(self,session,identity):
|
|
129
|
+
with self.transaction()as cur:
|
|
130
|
+
principal=self._owner(cur,session);row=self._row(cur,identity)
|
|
131
|
+
if not row or row['account_id']!=principal.account_id or row['principal_key']!=principal.key or row['session_sha256']!=session.id:raise QueryMissing('Question unavailable')
|
|
132
|
+
if row['state']not in {'cancelled','expired'}:self._end(cur,row,'cancelled')
|
|
133
|
+
|
|
134
|
+
def pending(self,session):
|
|
135
|
+
with self.transaction()as cur:
|
|
136
|
+
principal=self._owner(cur,session)
|
|
137
|
+
rows=cur.execute("SELECT * FROM enterprise_queries.requests WHERE account_id=%s AND principal_key=%s AND session_sha256=%s AND state IN('queued','running','ready')AND expires_at>clock_timestamp()ORDER BY created_at DESC LIMIT 3",(principal.account_id,principal.key,session.id)).fetchall()
|
|
138
|
+
return [{'id':str(row['id']),'status':row['state'],'question':self.auth.box.open(self.context(row,'question'),row['question_ciphertext'])['question']}for row in reversed(rows)]
|
|
139
|
+
|
|
140
|
+
def claim(self,identity):
|
|
141
|
+
with self.transaction()as cur:
|
|
142
|
+
row=self._row(cur,identity)
|
|
143
|
+
if not row:return None
|
|
144
|
+
if not row['current']:
|
|
145
|
+
if row['state']!='expired':self._end(cur,row,'expired')
|
|
146
|
+
return None
|
|
147
|
+
if row['state']not in {'queued','running'}or(row['state']=='running'and row['leased']):return None
|
|
148
|
+
try:principal=self._principal(cur,row['session_sha256'])
|
|
149
|
+
except SourceDenied:
|
|
150
|
+
self._end(cur,row,'cancelled');return None
|
|
151
|
+
if principal.account_id!=row['account_id']or principal.key!=row['principal_key']:self._end(cur,row,'cancelled');return None
|
|
152
|
+
if row['attempt']>=3:self._end(cur,row,'failed','unavailable');return None
|
|
153
|
+
question=self.auth.box.open(self.context(row,'question'),row['question_ciphertext'])['question'];query_text(question)
|
|
154
|
+
if self.auth.box.fingerprint(['question',row['account_id'],row['principal_key'],row['session_sha256'],str(row['request_key'])],question)!=row['question_mac']:raise SourceDenied('query_binding_invalid')
|
|
155
|
+
token=str(uuid.uuid4());row['attempt']+=1
|
|
156
|
+
cur.execute("UPDATE enterprise_queries.requests SET state='running',attempt=%s,lease_token=%s,lease_until=least(expires_at,clock_timestamp()+make_interval(secs=>%s)),result_ciphertext=NULL,error_code=NULL,updated_at=clock_timestamp()WHERE id=%s",(row['attempt'],token,self.lease_seconds,identity))
|
|
157
|
+
self._event(cur,row,'claimed')
|
|
158
|
+
return QueryLease(identity,token,principal,row['session_sha256'],question,row['attempt'])
|
|
159
|
+
|
|
160
|
+
def _lease(self,cur,lease):
|
|
161
|
+
row=self._row(cur,lease.id)
|
|
162
|
+
if not row or row['state']!='running'or not row['current']or not row['leased']or str(row['lease_token'])!=lease.token or row['attempt']!=lease.attempt:raise QueryLeaseLost('Question worker lease changed')
|
|
163
|
+
principal=self._principal(cur,row['session_sha256'])
|
|
164
|
+
if principal.account_id!=lease.principal.account_id or principal.key!=lease.principal.key:raise SourceDenied('company_identity_required')
|
|
165
|
+
return row
|
|
166
|
+
|
|
167
|
+
def guard(self,lease,*,renew=False):
|
|
168
|
+
with self.transaction()as cur:
|
|
169
|
+
row=self._lease(cur,lease)
|
|
170
|
+
if renew:cur.execute('UPDATE enterprise_queries.requests SET lease_until=least(expires_at,clock_timestamp()+make_interval(secs=>%s))WHERE id=%s',(self.lease_seconds,row['id']))
|
|
171
|
+
|
|
172
|
+
def complete(self,lease,receipt):
|
|
173
|
+
if not isinstance(receipt,dict)or receipt.get('question')!=lease.question or len(json.dumps(receipt,ensure_ascii=False,allow_nan=False).encode())>500000:raise ValueError('Invalid owned answer receipt')
|
|
174
|
+
with self.transaction()as cur:
|
|
175
|
+
row=self._lease(cur,lease);cipher=self.auth.box.seal(self.context(row,'answer'),receipt)
|
|
176
|
+
cur.execute("UPDATE enterprise_queries.requests SET state='ready',result_ciphertext=%s,lease_token=NULL,lease_until=NULL,updated_at=clock_timestamp()WHERE id=%s",(cipher,row['id']));self._event(cur,row,'ready')
|
|
177
|
+
|
|
178
|
+
def failed(self,lease,code,*,retry=False):
|
|
179
|
+
if code not in {'source','setup','signin','unavailable','answer'}:raise ValueError('Use an owned public failure code')
|
|
180
|
+
with self.transaction()as cur:
|
|
181
|
+
row=self._row(cur,lease.id)
|
|
182
|
+
if not row or row['state']!='running'or str(row['lease_token'])!=lease.token or row['attempt']!=lease.attempt:return
|
|
183
|
+
if retry and row['current']and row['attempt']<3:
|
|
184
|
+
cur.execute("UPDATE enterprise_queries.requests SET state='queued',lease_token=NULL,lease_until=NULL,error_code=NULL,updated_at=clock_timestamp()WHERE id=%s",(row['id'],))
|
|
185
|
+
self._enqueue(cur,row['id'],delay=5*row['attempt']);self._event(cur,row,'retry_pending')
|
|
186
|
+
else:self._end(cur,row,'failed',code)
|
|
187
|
+
|
|
188
|
+
def open_result(self,row):
|
|
189
|
+
if row['state']!='ready':raise QueryConflict('Question is not ready')
|
|
190
|
+
return self.auth.box.open(self.context(row,'answer'),row['result_ciphertext'])
|
|
191
|
+
|
|
192
|
+
def released(self,session,identity,attempt):
|
|
193
|
+
with self.transaction()as cur:
|
|
194
|
+
principal=self._owner(cur,session);row=self._row(cur,identity)
|
|
195
|
+
if not row or row['account_id']!=principal.account_id or row['principal_key']!=principal.key or row['session_sha256']!=session.id:raise QueryMissing('Question unavailable')
|
|
196
|
+
if not row['current']or row['state']!='ready'or row['attempt']!=attempt:raise QueryLeaseLost('Question changed before release')
|
|
197
|
+
self._event(cur,row,'released')
|
|
198
|
+
|
|
199
|
+
def requeue(self,session,identity,attempt):
|
|
200
|
+
with self.transaction()as cur:
|
|
201
|
+
principal=self._owner(cur,session);row=self._row(cur,identity)
|
|
202
|
+
if not row or row['account_id']!=principal.account_id or row['principal_key']!=principal.key or row['session_sha256']!=session.id:raise QueryMissing('Question unavailable')
|
|
203
|
+
if row['state']!='ready'or row['attempt']!=attempt:return
|
|
204
|
+
if not row['current']or row['attempt']>=3:self._end(cur,row,'failed','answer');return
|
|
205
|
+
cur.execute("UPDATE enterprise_queries.requests SET state='queued',result_ciphertext=NULL,error_code=NULL,updated_at=clock_timestamp()WHERE id=%s",(identity,));self._enqueue(cur,identity);self._event(cur,row,'retry_pending')
|
|
206
|
+
|
|
207
|
+
def recover(self,limit=50):
|
|
208
|
+
if type(limit)is not int or not 1<=limit<=100:raise ValueError('Use a bounded recovery batch')
|
|
209
|
+
with self.transaction()as cur:
|
|
210
|
+
rows=cur.execute("SELECT r.*,expires_at>clock_timestamp()AS current FROM enterprise_queries.requests r WHERE account_id=%s AND((state!='expired'AND expires_at<clock_timestamp())OR(state='running'AND lease_until<clock_timestamp())OR(state='queued'AND NOT EXISTS(SELECT 1 FROM enterprise_queries.procrastinate_jobs j WHERE j.task_name=%s AND j.args->>'query_id'=r.id::text AND j.status IN('todo','doing'))))ORDER BY updated_at FOR UPDATE OF r SKIP LOCKED LIMIT %s",(self.account_id,self.task.name,limit)).fetchall()
|
|
211
|
+
for row in rows:
|
|
212
|
+
if not row['current']:self._end(cur,row,'expired')
|
|
213
|
+
elif row['attempt']>=3:self._end(cur,row,'failed','unavailable')
|
|
214
|
+
else:
|
|
215
|
+
cur.execute("UPDATE enterprise_queries.requests SET state='queued',lease_token=NULL,lease_until=NULL,result_ciphertext=NULL,updated_at=clock_timestamp()WHERE id=%s",(row['id'],));self._enqueue(cur,row['id']);self._event(cur,row,'retry_pending')
|
|
216
|
+
return len(rows)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def producer_queries(auth,dsn,account_id,*,session_domain=None):
|
|
220
|
+
app=procrastinate.App(connector=procrastinate.SyncPsycopgConnector(conninfo=dsn,kwargs={'connect_timeout':5,'options':'-c search_path=pg_catalog,enterprise_queries,pg_temp'}))
|
|
221
|
+
def worker_only(query_id):raise RuntimeError('Run the owned dedicated query worker')
|
|
222
|
+
name,queue=task_identity(account_id)
|
|
223
|
+
task=app.task(name=name,queue=queue)(worker_only)
|
|
224
|
+
return EnterpriseQueryStore(auth,task,account_id,session_domain=session_domain)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def initialize_query_store(conn):
|
|
228
|
+
if procrastinate.__version__!='3.10.0' or conn.autocommit:raise ValueError('Use the reviewed queue version and an operator transaction')
|
|
229
|
+
body=Path(__file__).with_name('enterprise_queries_schema.sql').read_bytes()
|
|
230
|
+
conn.execute('CREATE SCHEMA enterprise_queries');conn.execute('SET LOCAL search_path=enterprise_queries,pg_catalog')
|
|
231
|
+
conn.execute(SchemaManager.get_schema());conn.execute(body.decode())
|
|
232
|
+
conn.execute('INSERT INTO enterprise_queries.schema_version VALUES(1,%s,%s)',(hashlib.sha256(body).hexdigest(),procrastinate.__version__))
|
|
233
|
+
for objects in ['TABLES','SEQUENCES','FUNCTIONS']:conn.execute('REVOKE ALL ON ALL '+objects+' IN SCHEMA enterprise_queries FROM PUBLIC')
|
|
234
|
+
conn.execute('REVOKE ALL ON SCHEMA enterprise_queries FROM PUBLIC')
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def grant_query_runtime(conn,role,*,worker=False):
|
|
238
|
+
if conn.autocommit or not isinstance(role,str)or not re.fullmatch('[A-Za-z0-9_.@-]{1,63}',role)or type(worker)is not bool:raise ValueError('Use an owned runtime role and operator transaction')
|
|
239
|
+
expected=hashlib.sha256(Path(__file__).with_name('enterprise_queries_schema.sql').read_bytes()).hexdigest()
|
|
240
|
+
if conn.execute('SELECT version,source_sha256,queue_version FROM enterprise_queries.schema_version').fetchone()!=(1,expected,'3.10.0'):raise ValueError('Review the exact fresh query schema before granting access')
|
|
241
|
+
flags=conn.execute('SELECT rolsuper,rolcreatedb,rolcreaterole,rolreplication,rolbypassrls,rolcanlogin FROM pg_roles WHERE rolname=%s',(role,)).fetchone()
|
|
242
|
+
if not flags or any(flags[:5])or not flags[5]or conn.execute("SELECT has_schema_privilege(%s,'enterprise_queries','CREATE')",(role,)).fetchone()[0]:raise ValueError('Use a nonadministrative role that does not own the query schema')
|
|
243
|
+
for table in ['memberships','source_mappings','company_bindings','schema_version']:
|
|
244
|
+
if conn.execute('SELECT has_table_privilege(%s,%s,%s)',(role,'enterprise_auth.'+table,'INSERT,UPDATE,DELETE,TRUNCATE,TRIGGER,REFERENCES')).fetchone()[0]:raise ValueError('Question runtime must not inherit authority-table write privileges')
|
|
245
|
+
target=sql.Identifier(role)
|
|
246
|
+
conn.execute(sql.SQL('GRANT USAGE ON SCHEMA enterprise_queries TO {}').format(target))
|
|
247
|
+
conn.execute(sql.SQL('GRANT SELECT ON enterprise_queries.schema_version TO {}').format(target))
|
|
248
|
+
conn.execute(sql.SQL('GRANT SELECT,INSERT,UPDATE ON enterprise_queries.requests TO {}').format(target))
|
|
249
|
+
conn.execute(sql.SQL('GRANT INSERT ON enterprise_queries.events TO {}').format(target))
|
|
250
|
+
conn.execute(sql.SQL('GRANT USAGE,SELECT ON ALL SEQUENCES IN SCHEMA enterprise_queries TO {}').format(target))
|
|
251
|
+
conn.execute(sql.SQL('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA enterprise_queries TO {}').format(target))
|
|
252
|
+
conn.execute(sql.SQL('GRANT SELECT,INSERT ON enterprise_queries.procrastinate_jobs,enterprise_queries.procrastinate_events TO {}').format(target))
|
|
253
|
+
if worker:
|
|
254
|
+
for table in ['procrastinate_jobs','procrastinate_events','procrastinate_workers','procrastinate_periodic_defers']:
|
|
255
|
+
conn.execute(sql.SQL('GRANT SELECT,INSERT,UPDATE,DELETE ON enterprise_queries.{} TO {}').format(sql.Identifier(table),target))
|