@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,275 @@
|
|
|
1
|
+
"""Owned Google Batch per-file jobs; no source/model/SQL credentials in workloads.
|
|
2
|
+
|
|
3
|
+
The VM transfer identity is trusted, distinct from the question worker. Only the
|
|
4
|
+
parser runnable is network-blocked. Its mounts are explicit local POSIX paths;
|
|
5
|
+
no FUSE, identity mounts, background listener or shared parser service is used.
|
|
6
|
+
Cloud IAM/network/provisioning remain required parts of the generated composition.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import hashlib
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
import requests
|
|
16
|
+
from enterprise_document_contract import MAX_INPUT
|
|
17
|
+
from enterprise_document_job import MAX_ENVELOPE, request, result, unique
|
|
18
|
+
from enterprise_sources import SourceUnavailable
|
|
19
|
+
from workflow_connection import google_metadata_token
|
|
20
|
+
from enterprise_document_jobs import DocumentJobLedger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def configuration(value):
|
|
24
|
+
fields = {'format', 'projectId', 'region', 'bucket', 'parserImage', 'transferImage',
|
|
25
|
+
'workerServiceAccount', 'vmServiceAccount', 'network', 'subnetwork'}
|
|
26
|
+
if not isinstance(value, dict) or set(value) != fields or value['format'] != 'enterprise-google-document-job-v1':
|
|
27
|
+
raise ValueError('Use the exact owned Google document-job configuration')
|
|
28
|
+
patterns = {'projectId': '[a-z][a-z0-9-]{4,28}[a-z0-9]', 'region': '[a-z]+-[a-z]+[1-9][0-9]?',
|
|
29
|
+
'bucket': '[a-z0-9][a-z0-9-]{1,61}[a-z0-9]'}
|
|
30
|
+
for key, pattern in patterns.items():
|
|
31
|
+
if not isinstance(value[key], str) or not re.fullmatch(pattern, value[key]): raise ValueError('Invalid owned Google job destination')
|
|
32
|
+
project, region = value['projectId'], value['region']
|
|
33
|
+
for key in ['workerServiceAccount', 'vmServiceAccount']:
|
|
34
|
+
if not isinstance(value[key], str) or not re.fullmatch('[a-z][a-z0-9-]{4,28}[a-z0-9]@' + re.escape(project) + r'\.iam\.gserviceaccount\.com', value[key]):
|
|
35
|
+
raise ValueError('Use distinct existing owned worker and VM service accounts')
|
|
36
|
+
if value['workerServiceAccount'] == value['vmServiceAccount']: raise ValueError('Parser transfer VM must not inherit worker authority')
|
|
37
|
+
for key in ['parserImage', 'transferImage']:
|
|
38
|
+
if not isinstance(value[key], str) or not re.fullmatch(re.escape(region + '-docker.pkg.dev/' + project + '/') + r'[a-z][a-z0-9_-]{0,62}/[a-z][a-z0-9_-]{0,62}(?:/[a-z][a-z0-9_-]{0,62}){0,3}@sha256:[0-9a-f]{64}', value[key]):
|
|
39
|
+
raise ValueError('Use immutable owned regional Artifact Registry images')
|
|
40
|
+
for key, prefix in [('network', 'projects/' + project + '/global/networks/'),
|
|
41
|
+
('subnetwork', 'projects/' + project + '/regions/' + region + '/subnetworks/')]:
|
|
42
|
+
if not isinstance(value[key], str) or not re.fullmatch(re.escape(prefix) + '[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?', value[key]):
|
|
43
|
+
raise ValueError('Use explicit same-project regional private networking')
|
|
44
|
+
return dict(value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def identity(value):
|
|
48
|
+
if not isinstance(value, str) or not re.fullmatch('[0-9a-f]{64}', value): raise ValueError('Invalid owned per-file identity')
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def object_key(identifier, side):
|
|
53
|
+
if side not in {'input', 'result'}: raise ValueError('Use the owned input or result object')
|
|
54
|
+
return 'document-jobs/' + identity(identifier) + '/' + side
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def transfer_metadata(config, metadata, generation):
|
|
58
|
+
if (not isinstance(metadata, dict) or set(metadata) != {'format', 'requestId', 'sha256', 'mediaType'} or
|
|
59
|
+
metadata['format'] != 'enterprise-document-job-v1' or not isinstance(metadata['sha256'], str) or
|
|
60
|
+
not re.fullmatch('[0-9a-f]{64}', metadata['sha256'])):
|
|
61
|
+
raise ValueError('Use the exact byte-bound document request')
|
|
62
|
+
identity(metadata['requestId'])
|
|
63
|
+
from enterprise_document_contract import media_type
|
|
64
|
+
if media_type(metadata['mediaType']) != metadata['mediaType']: raise ValueError('Use canonical document MIME')
|
|
65
|
+
if not isinstance(generation, str) or not re.fullmatch('[1-9][0-9]{0,29}', generation): raise ValueError('Use the immutable native input generation')
|
|
66
|
+
return {'format': 'enterprise-google-document-transfer-v1', 'projectId': config['projectId'],
|
|
67
|
+
'region': config['region'], 'bucket': config['bucket'], 'request': dict(metadata), 'inputGeneration': generation}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def job_spec(config, metadata, generation):
|
|
71
|
+
owned = configuration(config)
|
|
72
|
+
transfer = transfer_metadata(owned, metadata, generation)
|
|
73
|
+
base = '/mnt/disks/share/document-' + metadata['requestId']
|
|
74
|
+
common = '--read-only --cap-drop=ALL --security-opt=no-new-privileges:true --pids-limit=128 --log-driver=none'
|
|
75
|
+
def bridge(mode, user):
|
|
76
|
+
return {'container': {'imageUri': owned['transferImage'], 'entrypoint': 'python',
|
|
77
|
+
'commands': ['-I', '/app/enterprise_google_transfer.py', mode],
|
|
78
|
+
'volumes': [base + ':/work:' + ('rw' if mode == 'download' else 'ro')],
|
|
79
|
+
'options': common + ' --user=' + user + (' --cap-add=CHOWN' if mode == 'download' else '')},
|
|
80
|
+
'environment': {'variables': {'DOCUMENT_JOB': json.dumps(transfer, separators=(',', ':'))}},
|
|
81
|
+
'timeout': '25s'}
|
|
82
|
+
parser = {'container': {'imageUri': owned['parserImage'], 'entrypoint': 'python',
|
|
83
|
+
'commands': ['-I', '/app/enterprise_document_cli.py', '--job'],
|
|
84
|
+
'volumes': [base + '/input:/input:ro', base + '/output:/output:rw'],
|
|
85
|
+
'options': common + ' --user=10001:10001 --tmpfs=/tmp:rw,noexec,nosuid,size=128m',
|
|
86
|
+
'blockExternalNetwork': True}, 'timeout': '110s'}
|
|
87
|
+
return {'taskGroups': [{'taskCount': '1', 'parallelism': '1', 'taskCountPerNode': '1',
|
|
88
|
+
'taskSpec': {'runnables': [bridge('download', '0:0'), parser, bridge('upload', '10001:10001')],
|
|
89
|
+
'maxRetryCount': 0, 'maxRunDuration': '160s', 'computeResource': {'cpuMilli': '2000', 'memoryMib': '2048'}}}],
|
|
90
|
+
'allocationPolicy': {'location': {'allowedLocations': ['regions/' + owned['region']]},
|
|
91
|
+
'serviceAccount': {'email': owned['vmServiceAccount']},
|
|
92
|
+
'network': {'networkInterfaces': [{'network': owned['network'], 'subnetwork': owned['subnetwork'], 'noExternalIpAddress': True}]},
|
|
93
|
+
'instances': [{'policy': {'machineType': 'e2-standard-2', 'provisioningModel': 'STANDARD'}, 'blockProjectSshKeys': True}]},
|
|
94
|
+
'logsPolicy': {'destination': 'DESTINATION_UNSPECIFIED'}, 'labels': {'atmem-workload': 'document-parser'}}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class GoogleDocumentApi:
|
|
98
|
+
def __init__(self, *, session=None, credential=None):
|
|
99
|
+
self.session = session or requests.Session(); self.session.trust_env = False
|
|
100
|
+
self.credential = credential or google_metadata_token
|
|
101
|
+
|
|
102
|
+
def send(self, method, url, *, body=None, data=None, params=None, limit=32768, allow_missing=False):
|
|
103
|
+
# Destinations are constructed internally, never supplied by a document,
|
|
104
|
+
# browser, model or source redirect. No environment proxies or redirects.
|
|
105
|
+
if not url.startswith(('https://batch.googleapis.com/v1/projects/', 'https://storage.googleapis.com/storage/v1/b/',
|
|
106
|
+
'https://storage.googleapis.com/upload/storage/v1/b/')):
|
|
107
|
+
raise ValueError('Use the fixed native document APIs')
|
|
108
|
+
try:
|
|
109
|
+
token = self.credential()
|
|
110
|
+
if not isinstance(token, str) or not 8 <= len(token) <= 20000 or any(ord(c) < 33 or ord(c) > 126 for c in token): raise ValueError()
|
|
111
|
+
started = time.monotonic()
|
|
112
|
+
headers = {'Authorization': 'Bearer ' + token}
|
|
113
|
+
if data is not None: headers['Content-Type'] = 'application/octet-stream'
|
|
114
|
+
with self.session.request(method, url, headers=headers, json=body, data=data, params=params,
|
|
115
|
+
stream=True, allow_redirects=False, timeout=(2, 8)) as response:
|
|
116
|
+
if allow_missing and response.status_code == 404: return None
|
|
117
|
+
if response.status_code not in {200, 201, 204}: raise ValueError()
|
|
118
|
+
raw = bytearray()
|
|
119
|
+
for chunk in response.iter_content(16384):
|
|
120
|
+
raw.extend(chunk)
|
|
121
|
+
if len(raw) > limit or time.monotonic() - started > 15: raise ValueError()
|
|
122
|
+
return bytes(raw)
|
|
123
|
+
except Exception:
|
|
124
|
+
raise SourceUnavailable('document_job_unavailable') from None
|
|
125
|
+
|
|
126
|
+
def json(self, *args, **kwargs):
|
|
127
|
+
raw = self.send(*args, **kwargs)
|
|
128
|
+
if raw is None: return None
|
|
129
|
+
try:
|
|
130
|
+
value = json.loads(raw, object_pairs_hook=unique)
|
|
131
|
+
if not isinstance(value, dict): raise ValueError()
|
|
132
|
+
return value
|
|
133
|
+
except Exception: raise SourceUnavailable('document_job_unavailable') from None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class GoogleDocumentObjects:
|
|
137
|
+
def __init__(self, bucket, api):
|
|
138
|
+
if not isinstance(bucket, str) or not re.fullmatch('[a-z0-9][a-z0-9-]{1,61}[a-z0-9]', bucket): raise ValueError('Use the owned ephemeral bucket')
|
|
139
|
+
self.bucket, self.api = bucket, api
|
|
140
|
+
self.base = 'https://storage.googleapis.com/storage/v1/b/' + bucket
|
|
141
|
+
|
|
142
|
+
def url(self, identifier, side): return self.base + '/o/' + quote(object_key(identifier, side), safe='')
|
|
143
|
+
|
|
144
|
+
def put(self, identifier, side, raw):
|
|
145
|
+
maximum = MAX_INPUT if side == 'input' else MAX_ENVELOPE
|
|
146
|
+
if not isinstance(raw, bytes) or not 0 < len(raw) <= maximum: raise ValueError('Invalid bounded document object')
|
|
147
|
+
value = self.api.json('POST', 'https://storage.googleapis.com/upload/storage/v1/b/' + self.bucket + '/o',
|
|
148
|
+
params={'uploadType': 'media', 'name': object_key(identifier, side), 'ifGenerationMatch': '0', 'fields': 'generation'}, data=raw)
|
|
149
|
+
generation = value.get('generation')
|
|
150
|
+
if not isinstance(generation, str) or not re.fullmatch('[1-9][0-9]{0,29}', generation): raise SourceUnavailable('document_job_unavailable')
|
|
151
|
+
return generation
|
|
152
|
+
|
|
153
|
+
def load(self, identifier, side, generation=None):
|
|
154
|
+
params = {'alt': 'media'}
|
|
155
|
+
if generation is not None:
|
|
156
|
+
if not isinstance(generation, str) or not re.fullmatch('[1-9][0-9]{0,29}', generation): raise ValueError('Invalid native generation')
|
|
157
|
+
params['generation'] = generation
|
|
158
|
+
return self.api.send('GET', self.url(identifier, side), params=params, limit=MAX_INPUT if side == 'input' else MAX_ENVELOPE)
|
|
159
|
+
|
|
160
|
+
def delete(self, identifier, side): self.api.send('DELETE', self.url(identifier, side), allow_missing=True)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class GoogleBatchDocuments:
|
|
164
|
+
"""Controller lifecycle; successful delete requests alone do not prove exit."""
|
|
165
|
+
def __init__(self, config, *, api=None, ledger=None, deadline=140, poll_seconds=1):
|
|
166
|
+
self.config = MappingProxyType(configuration(config))
|
|
167
|
+
if type(deadline) is not int or not 1 <= deadline <= 140 or type(poll_seconds) not in {int, float} or not .01 <= poll_seconds <= 2:
|
|
168
|
+
raise ValueError('Use bounded owned document deadlines')
|
|
169
|
+
self.api = api or GoogleDocumentApi()
|
|
170
|
+
self.objects = GoogleDocumentObjects(self.config['bucket'], self.api)
|
|
171
|
+
self.deadline, self.poll_seconds = deadline, poll_seconds
|
|
172
|
+
self.parent = 'projects/' + self.config['projectId'] + '/locations/' + self.config['region']
|
|
173
|
+
self.base = 'https://batch.googleapis.com/v1/' + self.parent
|
|
174
|
+
self.target = hashlib.sha256(json.dumps({key:self.config[key]for key in ['projectId','region','bucket']},sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
|
175
|
+
self.ledger = ledger
|
|
176
|
+
|
|
177
|
+
def __call__(self, raw, mime):
|
|
178
|
+
raise SourceUnavailable('source_validation_required')
|
|
179
|
+
|
|
180
|
+
def bind_runtime(self, provider, environment):
|
|
181
|
+
if (provider != 'gcp' or environment.get('PROJECT_ID') != self.config['projectId'] or
|
|
182
|
+
environment.get('REGION') != self.config['region'] or
|
|
183
|
+
environment.get('DATABASE_SERVICE_ACCOUNT') != self.config['workerServiceAccount']):
|
|
184
|
+
raise ValueError('Document execution must match the selected provider, project, region and worker identity')
|
|
185
|
+
|
|
186
|
+
def attach_runtime(self, auth, account_id):
|
|
187
|
+
if self.ledger is None:self.ledger=DocumentJobLedger(auth,account_id,self.target)
|
|
188
|
+
elif (not isinstance(self.ledger,DocumentJobLedger)or self.ledger.auth is not auth or
|
|
189
|
+
self.ledger.account_id!=account_id or self.ledger.target!=self.target):
|
|
190
|
+
raise ValueError('Use the owned persistent worker cleanup ledger')
|
|
191
|
+
|
|
192
|
+
def reconcile(self, *, limit=5):
|
|
193
|
+
if self.ledger is None:raise SourceUnavailable('document_job_recovery_required')
|
|
194
|
+
if type(limit)is not int or not 1<=limit<=20:raise ValueError('Use a bounded cleanup batch')
|
|
195
|
+
counts={'claimed':0,'retired':0,'settling':0,'failed':0}
|
|
196
|
+
for _ in range(limit):
|
|
197
|
+
lease=self.ledger.claim_due()
|
|
198
|
+
if lease is None:break
|
|
199
|
+
counts['claimed']+=1
|
|
200
|
+
try:
|
|
201
|
+
self.cleanup(self.parent+'/jobs/document-'+lease.request_id[:32],lease.request_id)
|
|
202
|
+
except Exception:
|
|
203
|
+
self.ledger.failed(lease);counts['failed']+=1
|
|
204
|
+
else:
|
|
205
|
+
retired=self.ledger.completed(lease);counts['retired'if retired else'settling']+=1
|
|
206
|
+
return counts
|
|
207
|
+
|
|
208
|
+
def from_source(self, raw, mime, guard):
|
|
209
|
+
if not callable(guard): raise ValueError('Supply the current source/company/job check')
|
|
210
|
+
identifier = uuid.uuid4().hex + uuid.uuid4().hex
|
|
211
|
+
metadata = request(raw, mime, identifier)
|
|
212
|
+
name = self.parent + '/jobs/document-' + identifier[:32]
|
|
213
|
+
url = 'https://batch.googleapis.com/v1/' + name
|
|
214
|
+
started = time.monotonic()
|
|
215
|
+
guard()
|
|
216
|
+
if self.ledger is None:raise SourceUnavailable('document_job_recovery_required')
|
|
217
|
+
lease=self.ledger.allocate(identifier)
|
|
218
|
+
def check():
|
|
219
|
+
self.ledger.guard(lease);guard();self.ledger.guard(lease)
|
|
220
|
+
# Cleanup always inspects this unique owned name, including lost create
|
|
221
|
+
# acknowledgements. It never lists/deletes another project's jobs/files.
|
|
222
|
+
try:
|
|
223
|
+
self.ledger.guard(lease)
|
|
224
|
+
generation = self.objects.put(identifier, 'input', raw)
|
|
225
|
+
self.ledger.advance(lease,'job_pending')
|
|
226
|
+
check()
|
|
227
|
+
value = self.api.json('POST', self.base + '/jobs', params={'jobId': name.rsplit('/', 1)[1]},
|
|
228
|
+
body=job_spec(dict(self.config), metadata, generation))
|
|
229
|
+
if value.get('name') != name: raise SourceUnavailable('document_job_unavailable')
|
|
230
|
+
self.ledger.advance(lease,'job_running')
|
|
231
|
+
while True:
|
|
232
|
+
check()
|
|
233
|
+
if time.monotonic() - started > self.deadline: raise SourceUnavailable('document_job_unavailable')
|
|
234
|
+
current = self.api.json('GET', url)
|
|
235
|
+
if current.get('name') != name: raise SourceUnavailable('document_job_unavailable')
|
|
236
|
+
state = current.get('status', {}).get('state')
|
|
237
|
+
if state == 'SUCCEEDED': break
|
|
238
|
+
if state not in {'QUEUED', 'SCHEDULED', 'RUNNING'}: raise SourceUnavailable('document_job_unavailable')
|
|
239
|
+
time.sleep(self.poll_seconds)
|
|
240
|
+
self.ledger.advance(lease,'succeeded')
|
|
241
|
+
check()
|
|
242
|
+
encoded = self.objects.load(identifier, 'result')
|
|
243
|
+
value = result(raw, mime, identifier, encoded)
|
|
244
|
+
check()
|
|
245
|
+
return value
|
|
246
|
+
finally:
|
|
247
|
+
cleanup=self.ledger.claim_owner(lease)
|
|
248
|
+
try:self.cleanup(name,identifier)
|
|
249
|
+
except BaseException:
|
|
250
|
+
self.ledger.failed(cleanup)
|
|
251
|
+
raise
|
|
252
|
+
else:self.ledger.completed(cleanup)
|
|
253
|
+
|
|
254
|
+
def cleanup(self, name, identifier):
|
|
255
|
+
failure = False
|
|
256
|
+
try:
|
|
257
|
+
operation = self.api.json('DELETE', 'https://batch.googleapis.com/v1/' + name, allow_missing=True)
|
|
258
|
+
if operation is not None:
|
|
259
|
+
expected = self.parent + '/operations/'
|
|
260
|
+
op = operation.get('name')
|
|
261
|
+
if not isinstance(op, str) or not re.fullmatch(re.escape(expected) + '[A-Za-z0-9_-]{1,200}', op): raise ValueError()
|
|
262
|
+
deadline = time.monotonic() + 30
|
|
263
|
+
while True:
|
|
264
|
+
if type(operation.get('done', False)) is not bool: raise ValueError()
|
|
265
|
+
if operation.get('done', False): break
|
|
266
|
+
if time.monotonic() > deadline: raise ValueError()
|
|
267
|
+
time.sleep(self.poll_seconds)
|
|
268
|
+
operation = self.api.json('GET', 'https://batch.googleapis.com/v1/' + op)
|
|
269
|
+
if operation.get('name') != op: raise ValueError()
|
|
270
|
+
if operation.get('error'): raise ValueError()
|
|
271
|
+
except Exception: failure = True
|
|
272
|
+
for side in ['input', 'result']:
|
|
273
|
+
try: self.objects.delete(identifier, side)
|
|
274
|
+
except Exception: failure = True
|
|
275
|
+
if failure: raise SourceUnavailable('document_job_cleanup_unavailable') from None
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Trusted, sequential Google Batch byte transfer; never execute a document.
|
|
2
|
+
|
|
3
|
+
Only the download/upload runnables have native VM network/identity access. They
|
|
4
|
+
use fixed native APIs and one bounded per-file mount. The parser never runs in
|
|
5
|
+
this image, and no service is left listening for it.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sys
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
12
|
+
from enterprise_document_contract import MAX_INPUT
|
|
13
|
+
from enterprise_document_job import MAX_ENVELOPE, read_file, request, result, unique
|
|
14
|
+
from enterprise_google_jobs import GoogleDocumentApi, GoogleDocumentObjects, transfer_metadata
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def metadata(encoded):
|
|
18
|
+
if not isinstance(encoded, str) or not 0 < len(encoded.encode()) <= 4096: raise ValueError('Invalid owned transfer metadata')
|
|
19
|
+
value = json.loads(encoded, object_pairs_hook=unique)
|
|
20
|
+
fields = {'format', 'projectId', 'region', 'bucket', 'request', 'inputGeneration'}
|
|
21
|
+
if not isinstance(value, dict) or set(value) != fields or value['format'] != 'enterprise-google-document-transfer-v1': raise ValueError('Invalid owned transfer metadata')
|
|
22
|
+
import re
|
|
23
|
+
for key, pattern in {'projectId': '[a-z][a-z0-9-]{4,28}[a-z0-9]', 'region': '[a-z]+-[a-z]+[1-9][0-9]?', 'bucket': '[a-z0-9][a-z0-9-]{1,61}[a-z0-9]'}.items():
|
|
24
|
+
if not isinstance(value[key], str) or not re.fullmatch(pattern, value[key]): raise ValueError('Invalid owned transfer destination')
|
|
25
|
+
if value != transfer_metadata(value, value['request'], value['inputGeneration']): raise ValueError('Invalid owned transfer metadata')
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def directory(parent, name, *, create=False):
|
|
30
|
+
if create: os.mkdir(name, 0o700, dir_fd=parent)
|
|
31
|
+
return os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parent)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def download(value, objects, mount='/work'):
|
|
35
|
+
"""Root/CHOWN only to prepare isolated UID10001 mounts, then this exits."""
|
|
36
|
+
expected = value['request']; identifier = expected['requestId']
|
|
37
|
+
raw = objects.load(identifier, 'input', value['inputGeneration'])
|
|
38
|
+
if request(raw, expected['mediaType'], identifier) != expected: raise ValueError('Document input binding changed')
|
|
39
|
+
root = source = target = None
|
|
40
|
+
try:
|
|
41
|
+
root = os.open(mount, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
42
|
+
# Refuse previous per-file state, even if contents appear compatible.
|
|
43
|
+
source = directory(root, 'input', create=True)
|
|
44
|
+
target = directory(root, 'output', create=True)
|
|
45
|
+
for name, content in [('document', raw), ('request.json', json.dumps(expected, separators=(',', ':')).encode())]:
|
|
46
|
+
fd = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o400, dir_fd=source)
|
|
47
|
+
with os.fdopen(fd, 'wb') as stream:
|
|
48
|
+
stream.write(content); stream.flush(); os.fsync(stream.fileno()); os.fchown(stream.fileno(), 10001, 10001)
|
|
49
|
+
os.fchmod(source, 0o500); os.fchown(source, 10001, 10001); os.fchown(target, 10001, 10001)
|
|
50
|
+
os.fsync(source); os.fsync(target); os.fsync(root)
|
|
51
|
+
finally:
|
|
52
|
+
for fd in (target, source, root):
|
|
53
|
+
if fd is not None: os.close(fd)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def upload(value, objects, mount='/work'):
|
|
57
|
+
root = source = target = None
|
|
58
|
+
try:
|
|
59
|
+
root = os.open(mount, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
60
|
+
source, target = directory(root, 'input'), directory(root, 'output')
|
|
61
|
+
raw = read_file(source, 'document', MAX_INPUT)
|
|
62
|
+
expected = value['request']
|
|
63
|
+
if request(raw, expected['mediaType'], expected['requestId']) != expected: raise ValueError('Document input binding changed')
|
|
64
|
+
encoded = read_file(target, 'result.json', MAX_ENVELOPE)
|
|
65
|
+
result(raw, expected['mediaType'], expected['requestId'], encoded)
|
|
66
|
+
objects.put(expected['requestId'], 'result', encoded)
|
|
67
|
+
finally:
|
|
68
|
+
for fd in (target, source, root):
|
|
69
|
+
if fd is not None: os.close(fd)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main():
|
|
73
|
+
# No document content, token, upstream response or traceback enters job logs.
|
|
74
|
+
try:
|
|
75
|
+
if sys.argv[1:] not in [['download'], ['upload']]: raise ValueError()
|
|
76
|
+
mode = sys.argv[1]
|
|
77
|
+
if os.getuid() != (0 if mode == 'download' else 10001): raise ValueError()
|
|
78
|
+
value = metadata(os.environ['DOCUMENT_JOB'])
|
|
79
|
+
objects = GoogleDocumentObjects(value['bucket'], GoogleDocumentApi())
|
|
80
|
+
(download if mode == 'download' else upload)(value, objects)
|
|
81
|
+
except Exception:
|
|
82
|
+
raise SystemExit(2) from None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == '__main__': main()
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Company-session API and native source consent callbacks.
|
|
2
|
+
|
|
3
|
+
Existing company login supplies the verified API bearer token at bootstrap. The
|
|
4
|
+
browser cannot choose an account, source account, model, URL or permission mapping.
|
|
5
|
+
Native secrets/model/decoding/SQL transports are separate owned adapters.
|
|
6
|
+
"""
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import html
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from urllib.parse import parse_qs,urlsplit
|
|
12
|
+
from fastapi import FastAPI,Request
|
|
13
|
+
from fastapi.responses import JSONResponse,HTMLResponse,RedirectResponse,Response
|
|
14
|
+
from starlette.concurrency import run_in_threadpool
|
|
15
|
+
from enterprise_identity import trusted_https
|
|
16
|
+
from enterprise_sources import SourceDenied,SourceUnavailable,checked_url
|
|
17
|
+
from public_faq import InvalidAnswer,object_pairs
|
|
18
|
+
|
|
19
|
+
COOKIE='__Host-enterprise-session'
|
|
20
|
+
CSRF='x-csrf-token'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class SourceConnection:
|
|
25
|
+
label: str
|
|
26
|
+
kind: str
|
|
27
|
+
driver: object
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def problem(code,status):
|
|
31
|
+
messages={'signin':'Sign in with your company account to continue.','forbidden':'Your account cannot access this workspace. Ask your workspace owner for access.','request':'Check your question or refresh this page and try again.','setup':'Your workspace owner needs to enable a company source for your account.','source':'Connect your company sources before asking a question.','busy':'Your source connection is updating. Try again in a moment.','capacity':'You already have questions processing. Wait for one to finish or cancel it before asking another.','unavailable':'Your workspace could not finish this answer. Please try again shortly.','answer':'This answer could not be verified against your current sources. Please try again.'}
|
|
32
|
+
return JSONResponse({'error':{'code':code,'message':messages[code]}},status_code=status)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def callback_notice(label):
|
|
36
|
+
title='We could not finish connecting '+label
|
|
37
|
+
return HTMLResponse('<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>'+html.escape(title)+'</title><main><h1>'+html.escape(title)+'</h1><p>Return to your workspace, sign in if needed, and choose Connect again.</p><a href="/">Return to your workspace</a></main></html>',status_code=400)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_app(identity,sessions,connections,make_engine,*,public_origin,web_config=None,static_root=None,answers=None,allow_synchronous=True):
|
|
41
|
+
if type(allow_synchronous)is not bool:raise ValueError('Use the owned question transport mode')
|
|
42
|
+
trusted_https(public_origin);origin=urlsplit(public_origin)
|
|
43
|
+
if origin.path not in {'','/'}or public_origin.endswith('/'):raise ValueError('Use the canonical owned HTTPS origin without a trailing slash')
|
|
44
|
+
if not isinstance(connections,dict)or not 1<=len(connections)<=20 or not callable(make_engine):raise ValueError('Supply owned native source and application adapters')
|
|
45
|
+
grants=set()
|
|
46
|
+
for alias,entry in connections.items():
|
|
47
|
+
if not isinstance(alias,str)or not re.fullmatch(r'[a-z][a-z0-9_-]{0,31}',alias)or not isinstance(entry,SourceConnection)or entry.kind not in {'graph','confluence'}or not isinstance(entry.label,str)or not 1<=len(entry.label)<=80:raise ValueError('Invalid configured source connection')
|
|
48
|
+
driver=entry.driver
|
|
49
|
+
if driver.store is not sessions.auth or driver.redirect!=public_origin+'/oauth/callback/'+alias:raise ValueError('Source consent must use the owned store and exact callback')
|
|
50
|
+
key=(entry.kind,driver.client,getattr(driver,'tenant',None))
|
|
51
|
+
if key in grants:raise ValueError('Use one consent/cache per vendor grant and share it across its document sources')
|
|
52
|
+
grants.add(key)
|
|
53
|
+
app=FastAPI(docs_url=None,redoc_url=None,openapi_url=None)
|
|
54
|
+
callback_paths={'/oauth/callback/'+alias for alias in connections}
|
|
55
|
+
auth=sessions.auth
|
|
56
|
+
if answers is not None and(answers.store.auth is not auth or answers.store.account_id!=identity.account_id or answers.connections!=connections):raise ValueError('Queued answers must use the same owned account, authority and sources')
|
|
57
|
+
csp="default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
|
|
58
|
+
if web_config is not None:
|
|
59
|
+
if answers is None:raise ValueError('The browser workspace requires the durable question service')
|
|
60
|
+
from enterprise_web import install_web
|
|
61
|
+
csp=install_web(app,identity,connections,web_config,static_root)
|
|
62
|
+
def cookie(request):
|
|
63
|
+
fields=[part.strip().split('=',1)for header in request.headers.getlist('cookie')for part in header.split(';')]
|
|
64
|
+
matches=[value for field,*value in fields if field==COOKIE]
|
|
65
|
+
if len(matches)>1:raise SourceDenied('company_identity_required')
|
|
66
|
+
return request.cookies.get(COOKIE,'')
|
|
67
|
+
async def actor(request,*,csrf=False):
|
|
68
|
+
session=await run_in_threadpool(sessions.authenticate,cookie(request))
|
|
69
|
+
if session.principal.account_id!=identity.account_id:raise SourceDenied('company_identity_required')
|
|
70
|
+
if csrf:
|
|
71
|
+
if request.headers.get('origin')!=public_origin:raise SourceDenied('browser_request_denied')
|
|
72
|
+
session.require_csrf(request.headers.get(CSRF,''))
|
|
73
|
+
return session
|
|
74
|
+
async def body(request,limit=8192):
|
|
75
|
+
data=bytearray()
|
|
76
|
+
async for chunk in request.stream():
|
|
77
|
+
data.extend(chunk)
|
|
78
|
+
if len(data)>limit:raise ValueError('Body exceeds the owned request limit')
|
|
79
|
+
return bytes(data)
|
|
80
|
+
async def json_body(request):
|
|
81
|
+
if request.headers.get('content-type','').split(';')[0].strip().lower()!='application/json':raise ValueError('Use JSON')
|
|
82
|
+
value=json.loads(await body(request),object_pairs_hook=object_pairs)
|
|
83
|
+
if not isinstance(value,dict):raise ValueError('Use an object')
|
|
84
|
+
return value
|
|
85
|
+
def source(alias):
|
|
86
|
+
if alias not in connections:raise ValueError('Unknown source')
|
|
87
|
+
return connections[alias]
|
|
88
|
+
@app.middleware('http')
|
|
89
|
+
async def boundary(request,next_handler):
|
|
90
|
+
try:
|
|
91
|
+
callback=request.url.path in callback_paths
|
|
92
|
+
supplied=request.headers.get('origin')
|
|
93
|
+
if not callback and supplied and supplied!=public_origin:response=problem('forbidden',403)
|
|
94
|
+
else:response=await next_handler(request)
|
|
95
|
+
except SourceDenied as error:
|
|
96
|
+
code=str(error)
|
|
97
|
+
if code in {'company_identity_required','company_identity_expired'}:response=problem('signin',401)
|
|
98
|
+
elif code=='source_consent_unavailable':response=problem('unavailable',503)
|
|
99
|
+
elif code=='source_mapping_required':response=problem('setup',403)
|
|
100
|
+
elif code in {'source_reconnection_required','source_consent_required'}:response=problem('source',409)
|
|
101
|
+
else:response=problem('forbidden',403)
|
|
102
|
+
except SourceUnavailable as error:response=problem('capacity',429)if str(error)=='query_capacity_reached'else problem('busy'if str(error)=='source_refresh_in_progress'else'unavailable',503)
|
|
103
|
+
except InvalidAnswer:response=problem('answer',503)
|
|
104
|
+
except(ValueError,UnicodeError,RecursionError):response=problem('request',400)
|
|
105
|
+
except Exception:response=problem('unavailable',503)
|
|
106
|
+
response.headers.update({'Cache-Control':'no-store','Pragma':'no-cache','X-Content-Type-Options':'nosniff','Referrer-Policy':'no-referrer','Strict-Transport-Security':'max-age=31536000','Content-Security-Policy':csp})
|
|
107
|
+
return response
|
|
108
|
+
@app.get('/healthz')
|
|
109
|
+
def health():return {'status':'running'}
|
|
110
|
+
@app.post('/api/session')
|
|
111
|
+
async def bootstrap(request:Request):
|
|
112
|
+
if request.headers.get('origin')!=public_origin:raise SourceDenied('browser_request_denied')
|
|
113
|
+
if await json_body(request)!={}:raise ValueError('No caller identity fields')
|
|
114
|
+
header=request.headers.get('authorization','')
|
|
115
|
+
principal=await run_in_threadpool(identity.verify,header[7:]if header.startswith('Bearer ')else'')
|
|
116
|
+
created=await run_in_threadpool(sessions.create,principal,cookie(request))
|
|
117
|
+
response=JSONResponse({'signedIn':True,'csrfToken':created.csrf})
|
|
118
|
+
response.set_cookie(COOKIE,created.token,max_age=created.max_age,secure=True,httponly=True,samesite='none',path='/')
|
|
119
|
+
return response
|
|
120
|
+
@app.get('/api/session')
|
|
121
|
+
async def describe(request:Request):
|
|
122
|
+
# Non-simple header blocks cross-site scripts/tags from retrieving CSRF.
|
|
123
|
+
# No CORS capability is granted to another origin.
|
|
124
|
+
if request.headers.get('x-workspace-request')!='session':raise SourceDenied('browser_request_denied')
|
|
125
|
+
user=await actor(request)
|
|
126
|
+
rows=[]
|
|
127
|
+
for alias,entry in connections.items():rows.append({'id':alias,'name':entry.label,'status':await run_in_threadpool(auth.connection_status,user.principal,entry.driver.source_id)})
|
|
128
|
+
return {'signedIn':True,'csrfToken':user.csrf,'sources':rows}
|
|
129
|
+
@app.delete('/api/session')
|
|
130
|
+
async def logout(request:Request):
|
|
131
|
+
user=await actor(request,csrf=True);await run_in_threadpool(sessions.revoke,user)
|
|
132
|
+
response=Response(status_code=204);response.delete_cookie(COOKIE,path='/',secure=True,httponly=True,samesite='none');return response
|
|
133
|
+
@app.post('/api/sources/{alias}/connect')
|
|
134
|
+
async def connect(alias:str,request:Request):
|
|
135
|
+
user=await actor(request,csrf=True);entry=source(alias)
|
|
136
|
+
if await json_body(request)!={}:raise ValueError('No caller source or callback settings')
|
|
137
|
+
if await run_in_threadpool(auth.connection_status,user.principal,entry.driver.source_id)=='needs_setup':raise SourceDenied('source_mapping_required')
|
|
138
|
+
start=await run_in_threadpool(entry.driver.start,user.principal,user.browser_nonce)
|
|
139
|
+
checked_url(start.url,{'login.microsoftonline.com'if entry.kind=='graph'else'auth.atlassian.com'})
|
|
140
|
+
return {'authorizationUrl':start.url}
|
|
141
|
+
@app.delete('/api/sources/{alias}')
|
|
142
|
+
async def disconnect(alias:str,request:Request):
|
|
143
|
+
user=await actor(request,csrf=True);entry=source(alias);await run_in_threadpool(auth.disconnect,user.principal,entry.driver.source_id);return Response(status_code=204)
|
|
144
|
+
@app.api_route('/oauth/callback/{alias}',methods=['GET','POST'])
|
|
145
|
+
async def callback(alias:str,request:Request):
|
|
146
|
+
entry=source(alias)
|
|
147
|
+
try:
|
|
148
|
+
if request.method!=('POST'if entry.kind=='graph'else'GET'):raise ValueError('Incorrect consent response method')
|
|
149
|
+
user=await actor(request)
|
|
150
|
+
if request.method=='POST':
|
|
151
|
+
if request.headers.get('content-type','').split(';')[0].strip().lower()!='application/x-www-form-urlencoded':raise ValueError('Incorrect consent content type')
|
|
152
|
+
params=parse_qs((await body(request,16384)).decode('utf-8'),keep_blank_values=True,strict_parsing=True,max_num_fields=12)
|
|
153
|
+
else:
|
|
154
|
+
if len(request.url.query)>16384:raise ValueError('Consent response too large')
|
|
155
|
+
params=parse_qs(request.url.query,keep_blank_values=True,strict_parsing=True,max_num_fields=12)
|
|
156
|
+
if any(len(values)!=1 for values in params.values())or not params.get('state'):raise ValueError('Invalid consent response')
|
|
157
|
+
if params.get('error'):
|
|
158
|
+
await run_in_threadpool(auth.consume,user.principal,entry.driver.source_id,user.browser_nonce,params['state'][0]);return callback_notice(entry.label)
|
|
159
|
+
if not params.get('code'):raise ValueError('Missing consent code')
|
|
160
|
+
await run_in_threadpool(entry.driver.finish,user.principal,user.browser_nonce,params['state'][0],params['code'][0])
|
|
161
|
+
return RedirectResponse('/?connection=connected&source='+alias,status_code=303)
|
|
162
|
+
except Exception:return callback_notice(entry.label)
|
|
163
|
+
@app.post('/api/ask')
|
|
164
|
+
async def ask(request:Request):
|
|
165
|
+
if not allow_synchronous:return JSONResponse({'error':{'code':'request','message':'Use the workspace to submit your question.'}},status_code=410)
|
|
166
|
+
user=await actor(request,csrf=True);value=await json_body(request)
|
|
167
|
+
if set(value)!={'question'}or not isinstance(value['question'],str)or not value['question'].strip()or len(value['question'])>1000:raise ValueError('Supply only the question')
|
|
168
|
+
def answer():
|
|
169
|
+
def authorize(subject):
|
|
170
|
+
if subject!=user.principal.key:raise SourceDenied('company_identity_required')
|
|
171
|
+
sessions.authenticate(cookie(request))
|
|
172
|
+
from enterprise_knowledge import current_engine
|
|
173
|
+
engine=current_engine(auth,connections,user.principal,make_engine,authorize)
|
|
174
|
+
result=engine.answer(value['question'],user.principal.key)
|
|
175
|
+
# Independent final company-session gate, beyond source document gates.
|
|
176
|
+
sessions.authenticate(cookie(request))
|
|
177
|
+
return result
|
|
178
|
+
return await run_in_threadpool(answer)
|
|
179
|
+
@app.post('/api/queries')
|
|
180
|
+
async def submit_question(request:Request):
|
|
181
|
+
user=await actor(request,csrf=True);value=await json_body(request)
|
|
182
|
+
if answers is None:raise SourceUnavailable('question_worker_unavailable')
|
|
183
|
+
if set(value)!={'question','requestId'}or not isinstance(value['question'],str)or not value['question'].strip()or len(value['question'])>1000:raise ValueError('Supply a question and request identity')
|
|
184
|
+
from enterprise_queries import QueryConflict
|
|
185
|
+
try:identity=await run_in_threadpool(answers.store.submit,user,value['question'],value['requestId'])
|
|
186
|
+
except QueryConflict:return JSONResponse({'error':{'code':'request','message':'This question could not be submitted. Please send it again.'}},status_code=409)
|
|
187
|
+
return JSONResponse({'id':identity,'status':'queued'},status_code=202)
|
|
188
|
+
@app.get('/api/queries/{query_id}')
|
|
189
|
+
async def question_status(query_id:str,request:Request):
|
|
190
|
+
if request.headers.get('x-workspace-request')!='session':raise SourceDenied('browser_request_denied')
|
|
191
|
+
user=await actor(request)
|
|
192
|
+
if answers is None:raise SourceUnavailable('question_worker_unavailable')
|
|
193
|
+
from enterprise_queries import QueryMissing
|
|
194
|
+
try:result=await run_in_threadpool(answers.status,user,query_id)
|
|
195
|
+
except QueryMissing:return JSONResponse({'error':{'code':'request','message':'This question is no longer available.'}},status_code=404)
|
|
196
|
+
await run_in_threadpool(sessions.authenticate,cookie(request))
|
|
197
|
+
return result
|
|
198
|
+
@app.get('/api/queries')
|
|
199
|
+
async def pending_questions(request:Request):
|
|
200
|
+
if request.headers.get('x-workspace-request')!='session':raise SourceDenied('browser_request_denied')
|
|
201
|
+
user=await actor(request)
|
|
202
|
+
if answers is None:raise SourceUnavailable('question_worker_unavailable')
|
|
203
|
+
return {'queries':await run_in_threadpool(answers.store.pending,user)}
|
|
204
|
+
@app.delete('/api/queries/{query_id}')
|
|
205
|
+
async def cancel_question(query_id:str,request:Request):
|
|
206
|
+
user=await actor(request,csrf=True)
|
|
207
|
+
if answers is None:raise SourceUnavailable('question_worker_unavailable')
|
|
208
|
+
from enterprise_queries import QueryMissing
|
|
209
|
+
try:await run_in_threadpool(answers.store.cancel,user,query_id)
|
|
210
|
+
except QueryMissing:return JSONResponse({'error':{'code':'request','message':'This question is no longer available.'}},status_code=404)
|
|
211
|
+
return Response(status_code=204)
|
|
212
|
+
return app
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Existing company API identity; no replacement IdP or source-token claims.
|
|
2
|
+
|
|
3
|
+
Issuer, API audience, JWKS URL, delegated scope and account are operator-owned
|
|
4
|
+
configuration. Live membership remains a separate required database check.
|
|
5
|
+
"""
|
|
6
|
+
from dataclasses import dataclass,field
|
|
7
|
+
import hashlib
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
from urllib.parse import urlsplit
|
|
11
|
+
import jwt
|
|
12
|
+
from enterprise_sources import SourceDenied
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def trusted_https(value):
|
|
16
|
+
try:parts=urlsplit(value);port=parts.port
|
|
17
|
+
except (TypeError,ValueError):raise ValueError('Invalid owned identity endpoint')from None
|
|
18
|
+
if parts.scheme!='https'or not parts.hostname or (port is not None and not 1<=port<=65535)or parts.username or parts.password or parts.query or parts.fragment or any(ord(c)<33 for c in value):raise ValueError('Invalid owned identity endpoint')
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class CompanyPrincipal:
|
|
24
|
+
account_id: str
|
|
25
|
+
issuer: str
|
|
26
|
+
subject: str = field(repr=False)
|
|
27
|
+
expires_at: int
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def key(self):return hashlib.sha256((self.issuer+'\0'+self.subject).encode()).hexdigest()
|
|
31
|
+
|
|
32
|
+
def require(self):
|
|
33
|
+
if type(self.expires_at)is not int or self.expires_at<=time.time()+5:raise SourceDenied('company_identity_expired')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CompanyIdentity:
|
|
37
|
+
def __init__(self,account_id,issuer,audience,jwks_uri,scope='knowledge.read',*,jwks=None):
|
|
38
|
+
if not isinstance(account_id,str)or not re.fullmatch(r'[a-zA-Z0-9_-]{1,128}',account_id):raise ValueError('Use the owned company account')
|
|
39
|
+
if not isinstance(audience,str)or not 1<=len(audience)<=300 or not isinstance(scope,str)or not re.fullmatch(r'[A-Za-z0-9_./:-]{1,200}',scope):raise ValueError('Use the owned API audience and delegated scope')
|
|
40
|
+
self.account_id=account_id;self.issuer=trusted_https(issuer);self.audience=audience;self.scope=scope
|
|
41
|
+
self.jwks_uri=trusted_https(jwks_uri)
|
|
42
|
+
self.jwks=jwks or jwt.PyJWKClient(self.jwks_uri,timeout=3,lifespan=300,cache_keys=False)
|
|
43
|
+
|
|
44
|
+
def verify(self,token):
|
|
45
|
+
try:
|
|
46
|
+
if not isinstance(token,str)or not 1<=len(token)<=16384:raise ValueError('Invalid token')
|
|
47
|
+
key=self.jwks.get_signing_key_from_jwt(token).key
|
|
48
|
+
claims=jwt.decode(token,key,algorithms=['RS256'],issuer=self.issuer,audience=self.audience,options={'require':['iss','sub','aud','exp','iat']})
|
|
49
|
+
scope=claims.get('scp',claims.get('scope'))
|
|
50
|
+
if not isinstance(scope,str)or self.scope not in scope.split()or claims['aud']!=self.audience or not isinstance(claims['sub'],str)or not 1<=len(claims['sub'].encode())<=255 or any(ord(c)<32 for c in claims['sub'])or any(type(claims[k])is not int for k in ['iat','exp'])or not 0<claims['exp']-claims['iat']<=3600:raise ValueError('Invalid company API credential')
|
|
51
|
+
principal=CompanyPrincipal(self.account_id,self.issuer,claims['sub'],claims['exp']);principal.require();return principal
|
|
52
|
+
except Exception:raise SourceDenied('company_identity_required')from None
|