@enterpriseai/cli 3.19.0 → 3.19.2
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/package.json +3 -3
- package/resources/deploy/eai-app-template/producer-pin.json +5 -5
- package/resources/deploy/eai-app-template/tests/source-unknown-deployment-evidence.test.mjs +2141 -1
- package/resources/gofer/.gofer-version +4 -357
- package/resources/gofer/agents-skills/eai/SKILL.md +365 -0
- package/resources/gofer/agents-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/agents-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/bash-scripts/create-new-feature.sh +3 -1
- package/resources/gofer/bash-scripts/sync-implementation-status.sh +4 -3
- package/resources/gofer/bash-scripts/update-agent-context.sh +7 -8
- package/resources/gofer/bash-scripts/validate-artifact.sh +10 -6
- package/resources/gofer/bash-scripts/verify-object-type-routing-workspace.sh +184 -0
- package/resources/gofer/claude-commands/eai-update.md +46 -0
- package/resources/gofer/claude-commands/eai.md +355 -0
- package/resources/gofer/claude-skills/eai/SKILL.md +365 -0
- package/resources/gofer/claude-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/claude-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/commands/0_gofer_start.md +440 -99
- package/resources/gofer/commands/0a_problem_validation.md +187 -27
- package/resources/gofer/commands/10_gofer_cloud.md +183 -23
- package/resources/gofer/commands/1_gofer_research.md +239 -30
- package/resources/gofer/commands/2_gofer_specify.md +239 -35
- package/resources/gofer/commands/3_gofer_plan.md +259 -43
- package/resources/gofer/commands/4_gofer_tasks.md +401 -81
- package/resources/gofer/commands/5_gofer_implement.md +375 -84
- package/resources/gofer/commands/6_gofer_validate.md +284 -53
- package/resources/gofer/commands/7_gofer_save.md +187 -26
- package/resources/gofer/commands/7a_stakeholder_comms.md +183 -23
- package/resources/gofer/commands/8_gofer_branding.md +183 -23
- package/resources/gofer/commands/9_gofer_tests.md +212 -23
- package/resources/gofer/commands/gofer_bootstrap_workspace.md +121 -8
- package/resources/gofer/commands/gofer_check_workspace.md +123 -8
- package/resources/gofer/commands/gofer_constitution.md +183 -23
- package/resources/gofer/commands/gofer_diagnose.md +183 -23
- package/resources/gofer/commands/gofer_eai_first_run.md +254 -25
- package/resources/gofer/commands/gofer_hydrate.md +183 -23
- package/resources/gofer/commands/gofer_personality.md +117 -6
- package/resources/gofer/commands/gofer_plan.md +117 -6
- package/resources/gofer/commands/gofer_side.md +117 -6
- package/resources/gofer/commands/gofer_spec_summary.md +183 -23
- package/resources/gofer/commands/gofer_tdd.md +183 -23
- package/resources/gofer/commands/gofer_vocabulary.md +185 -25
- package/resources/gofer/commands/gofer_zoom_out.md +183 -23
- package/resources/gofer/config/typesafe-learning-review.json +16 -0
- package/resources/gofer/config/typesafe-semantic-review.json +9 -0
- package/resources/gofer/copilot-instructions/gofer-cost.instructions.md +15 -6
- package/resources/gofer/copilot-instructions/typescript.instructions.md +1 -1
- package/resources/gofer/copilot-prompts/eai-update.prompt.md +58 -0
- package/resources/gofer/copilot-prompts/eai.prompt.md +367 -0
- package/resources/gofer/gemini/commands/gofer/eai-update.md +22 -0
- package/resources/gofer/gemini/commands/gofer/eai-update.toml +2 -0
- package/resources/gofer/gemini/commands/gofer/eai.md +357 -0
- package/resources/gofer/gemini/commands/gofer/eai.toml +2 -0
- package/resources/gofer/gemini/commands/gofer/manifest.json +3 -27
- package/resources/gofer/gemini/extension.json +4 -4
- package/resources/gofer/github-agents/eai.agent.md +83 -0
- package/resources/gofer/github-agents/gofer-business.agent.md +73 -4
- package/resources/gofer/github-agents/gofer-implement.agent.md +71 -2
- package/resources/gofer/github-agents/gofer-plan.agent.md +72 -2
- package/resources/gofer/github-agents/gofer-research.agent.md +72 -2
- package/resources/gofer/github-agents/gofer-validate.agent.md +72 -2
- package/resources/gofer/github-skills/eai/SKILL.md +365 -0
- package/resources/gofer/github-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/github-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/grok-skills/eai/SKILL.md +267 -14
- package/resources/gofer/grok-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/hook-scripts/user-prompt-submit.mjs +58 -13
- package/resources/gofer/instruction-templates/base/agents-base.md +8 -4
- package/resources/gofer/instruction-templates/gofer/gofer-claude.md +7 -5
- package/resources/gofer/instruction-templates/gofer/gofer-copilot.md +10 -8
- package/resources/gofer/memory/constitution.md +9 -7
- package/resources/gofer/memory/gofer-model-policy.yaml +23 -1
- package/resources/gofer/memory/lessons.md +0 -31
- package/resources/gofer/node-scripts/canonical-descriptions.mjs +1 -1
- package/resources/gofer/node-scripts/check-version-alignment.mjs +4 -7
- package/resources/gofer/node-scripts/delivery-lineage-corpus.mjs +156 -0
- package/resources/gofer/node-scripts/generate-commands.mjs +1073 -265
- package/resources/gofer/node-scripts/gofer-acceptance-check.mjs +143 -0
- package/resources/gofer/node-scripts/gofer-agent-catalog.mjs +271 -0
- package/resources/gofer/node-scripts/gofer-benchmark-executor.mjs +123 -0
- package/resources/gofer/node-scripts/gofer-benchmark-sign.mjs +62 -0
- package/resources/gofer/node-scripts/gofer-benchmark-signer.mjs +89 -0
- package/resources/gofer/node-scripts/gofer-benchmark.mjs +76 -0
- package/resources/gofer/node-scripts/gofer-blocker-control.mjs +274 -0
- package/resources/gofer/node-scripts/gofer-claude-adapter.mjs +154 -0
- package/resources/gofer/node-scripts/gofer-closed-loop-audit.mjs +19 -2
- package/resources/gofer/node-scripts/gofer-delivery-check.mjs +121 -0
- package/resources/gofer/node-scripts/gofer-encrypted-key.mjs +58 -0
- package/resources/gofer/node-scripts/gofer-execution-metrics.mjs +477 -0
- package/resources/gofer/node-scripts/gofer-execution-recovery.mjs +334 -0
- package/resources/gofer/node-scripts/gofer-grok-adapter.mjs +184 -0
- package/resources/gofer/node-scripts/gofer-heldout-corpus.mjs +73 -0
- package/resources/gofer/node-scripts/gofer-heldout-snapshot.mjs +286 -0
- package/resources/gofer/node-scripts/gofer-heldout-verifier.mjs +211 -0
- package/resources/gofer/node-scripts/gofer-host-capability.mjs +335 -0
- package/resources/gofer/node-scripts/gofer-independent-reviewer.mjs +130 -0
- package/resources/gofer/node-scripts/gofer-live-routing.mjs +109 -0
- package/resources/gofer/node-scripts/gofer-local-capability-issuer.mjs +71 -0
- package/resources/gofer/node-scripts/gofer-local-isolation.mjs +175 -0
- package/resources/gofer/node-scripts/gofer-local-settings-cleanup.mjs +388 -0
- package/resources/gofer/node-scripts/gofer-loop-audit.mjs +54 -3
- package/resources/gofer/node-scripts/gofer-native-adapter.mjs +567 -0
- package/resources/gofer/node-scripts/gofer-native-benchmark-dispatch.mjs +145 -0
- package/resources/gofer/node-scripts/gofer-native-runtime.mjs +169 -0
- package/resources/gofer/node-scripts/gofer-pre-edit-check.mjs +204 -0
- package/resources/gofer/node-scripts/gofer-priority-check.mjs +214 -0
- package/resources/gofer/node-scripts/gofer-response-check.mjs +56 -0
- package/resources/gofer/node-scripts/gofer-reviewed-learning.mjs +1232 -0
- package/resources/gofer/node-scripts/gofer-run-verified-task.mjs +330 -0
- package/resources/gofer/node-scripts/gofer-runtime-ledger.mjs +289 -0
- package/resources/gofer/node-scripts/gofer-semantic-drift.mjs +584 -0
- package/resources/gofer/node-scripts/gofer-surface-update.mjs +1178 -0
- package/resources/gofer/node-scripts/gofer-trust-bootstrap.mjs +77 -0
- package/resources/gofer/node-scripts/gofer-trusted-benchmark.mjs +102 -0
- package/resources/gofer/node-scripts/gofer-trusted-evaluator.mjs +274 -0
- package/resources/gofer/node-scripts/gofer-tty-prompt.mjs +34 -0
- package/resources/gofer/node-scripts/gofer-typesafe-credentials.mjs +435 -0
- package/resources/gofer/node-scripts/gofer-ui-preview.mjs +1212 -0
- package/resources/gofer/node-scripts/gofer-verified-execution.mjs +484 -0
- package/resources/gofer/node-scripts/gofer-verifier-key-ceremony.mjs +101 -0
- package/resources/gofer/node-scripts/gofer-worktree-layout.mjs +32 -0
- package/resources/gofer/node-scripts/object-type-routing-phase-bundle.mjs +360 -0
- package/resources/gofer/node-scripts/package-agent-plugin.mjs +574 -82
- package/resources/gofer/node-scripts/parse-stage-command.mjs +1 -1
- package/resources/gofer/node-scripts/render-delivery-lineage.mjs +178 -0
- package/resources/gofer/node-scripts/sync-extension-resources.mjs +78 -2
- package/resources/gofer/node-scripts/validate-object-type-identifiers.mjs +767 -0
- package/resources/gofer/node-scripts/validate-object-type-routing-workspace.mjs +1325 -0
- package/resources/gofer/node-scripts/validate-v4-resource-contract.d.mts +44 -0
- package/resources/gofer/node-scripts/validate-v4-resource-contract.mjs +957 -0
- package/resources/gofer/node-scripts/workspace-bootstrap-lib.mjs +685 -116
- package/resources/gofer/references/agent-catalog.json +126 -0
- package/resources/gofer/references/blocker-mediation.md +156 -0
- package/resources/gofer/references/business-updates-and-goal-checks.md +109 -0
- package/resources/gofer/references/delivery-lineage.md +48 -0
- package/resources/gofer/references/mvp-capability-validation.md +102 -0
- package/resources/gofer/references/platform/README.md +1 -1
- package/resources/gofer/references/platform/eai-app-template.md +15 -13
- package/resources/gofer/references/platform/eai-auth-access.md +104 -0
- package/resources/gofer/references/platform/eai-config-driven-ui.md +5 -5
- package/resources/gofer/references/platform/eai-error-catalog.yaml +77 -19
- package/resources/gofer/references/platform/eai-repo-contract.md +61 -23
- package/resources/gofer/references/platform/eai-service-patterns.md +102 -105
- package/resources/gofer/references/platform/eai.md +44 -8
- package/resources/gofer/references/platform/vertical-template.md +10 -9
- package/resources/gofer/references/priority-outcome-protection.md +152 -0
- package/resources/gofer/references/verified-agent-execution.md +179 -0
- package/resources/gofer/schemas/gofer-learning-trace-v1.json +70 -0
- package/resources/gofer/specify-commands/0_gofer_start.md +440 -99
- package/resources/gofer/specify-commands/0a_problem_validation.md +187 -27
- package/resources/gofer/specify-commands/10_gofer_cloud.md +183 -23
- package/resources/gofer/specify-commands/1_gofer_research.md +239 -30
- package/resources/gofer/specify-commands/2_gofer_specify.md +239 -35
- package/resources/gofer/specify-commands/3_gofer_plan.md +259 -43
- package/resources/gofer/specify-commands/4_gofer_tasks.md +401 -81
- package/resources/gofer/specify-commands/5_gofer_implement.md +375 -84
- package/resources/gofer/specify-commands/6_gofer_validate.md +284 -53
- package/resources/gofer/specify-commands/7_gofer_save.md +187 -26
- package/resources/gofer/specify-commands/7a_stakeholder_comms.md +183 -23
- package/resources/gofer/specify-commands/8_gofer_branding.md +183 -23
- package/resources/gofer/specify-commands/9_gofer_tests.md +212 -23
- package/resources/gofer/specify-commands/gofer_bootstrap_workspace.md +121 -8
- package/resources/gofer/specify-commands/gofer_check_workspace.md +123 -8
- package/resources/gofer/specify-commands/gofer_constitution.md +183 -23
- package/resources/gofer/specify-commands/gofer_diagnose.md +183 -23
- package/resources/gofer/specify-commands/gofer_eai_first_run.md +254 -25
- package/resources/gofer/specify-commands/gofer_hydrate.md +183 -23
- package/resources/gofer/specify-commands/gofer_personality.md +117 -6
- package/resources/gofer/specify-commands/gofer_plan.md +117 -6
- package/resources/gofer/specify-commands/gofer_side.md +117 -6
- package/resources/gofer/specify-commands/gofer_spec_summary.md +183 -23
- package/resources/gofer/specify-commands/gofer_tdd.md +183 -23
- package/resources/gofer/specify-commands/gofer_vocabulary.md +185 -25
- package/resources/gofer/specify-commands/gofer_zoom_out.md +183 -23
- package/resources/gofer/specify-config/object-type-routing.json +24 -0
- package/resources/gofer/specify-config/typesafe-learning-review.json +16 -0
- package/resources/gofer/specify-config/typesafe-semantic-review.json +9 -0
- package/resources/gofer/system-skills/eai/SKILL.md +365 -0
- package/resources/gofer/system-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/system-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/templates/build-map-template.md +64 -0
- package/resources/gofer/templates/business-scenarios-template.json +14 -0
- package/resources/gofer/templates/ciso-security-summary-template.md +3 -3
- package/resources/gofer/templates/context-bundle-template.md +4 -3
- package/resources/gofer/templates/contract-pack-template.md +8 -8
- package/resources/gofer/templates/cto-architecture-summary-template.md +3 -3
- package/resources/gofer/templates/delivery-lineage-template.json +8 -0
- package/resources/gofer/templates/eai-preflight-template.md +109 -68
- package/resources/gofer/templates/goal-ledger-template.json +1 -1
- package/resources/gofer/templates/gofer-model-policy.yaml +28 -6
- package/resources/gofer/templates/loop-contract-template.json +3 -1
- package/resources/gofer/templates/plan-template.md +8 -7
- package/resources/gofer/templates/priority-plan-template.json +29 -0
- package/resources/gofer/templates/proposal-review-template.md +6 -3
- package/resources/gofer/templates/release-capability-ledger-template.md +24 -0
- package/resources/gofer/templates/research-template.md +7 -6
- package/resources/gofer/templates/spec-summary-template.md +1 -1
- package/resources/gofer/templates/spec-template.md +45 -6
- package/resources/gofer/templates/tasks-template.md +12 -3
- package/resources/gofer/templates/ui-preview-brief-template.md +55 -5
- package/resources/gofer/templates/ui-review-log-template.md +21 -3
- package/resources/gofer/templates/ui-show-and-tell-template.md +46 -0
- package/resources/gofer/templates/visuals/capability-heatmap-template.md +2 -2
- package/resources/gofer/templates/working-backwards-prfaq-template.md +9 -8
- package/resources/linked-sources.json +4 -4
- package/resources/gofer/agents-skills/0_gofer_start/SKILL.md +0 -1287
- package/resources/gofer/agents-skills/0a_problem_validation/SKILL.md +0 -429
- package/resources/gofer/agents-skills/10_gofer_cloud/SKILL.md +0 -635
- package/resources/gofer/agents-skills/1_gofer_research/SKILL.md +0 -1159
- package/resources/gofer/agents-skills/2_gofer_specify/SKILL.md +0 -860
- package/resources/gofer/agents-skills/3_gofer_plan/SKILL.md +0 -899
- package/resources/gofer/agents-skills/4_gofer_tasks/SKILL.md +0 -760
- package/resources/gofer/agents-skills/5_gofer_implement/SKILL.md +0 -944
- package/resources/gofer/agents-skills/6_gofer_validate/SKILL.md +0 -2302
- package/resources/gofer/agents-skills/7_gofer_save/SKILL.md +0 -423
- package/resources/gofer/agents-skills/7a_stakeholder_comms/SKILL.md +0 -508
- package/resources/gofer/agents-skills/8_gofer_branding/SKILL.md +0 -315
- package/resources/gofer/agents-skills/9_gofer_tests/SKILL.md +0 -558
- package/resources/gofer/agents-skills/gofer_bootstrap_workspace/SKILL.md +0 -148
- package/resources/gofer/agents-skills/gofer_check_workspace/SKILL.md +0 -119
- package/resources/gofer/agents-skills/gofer_constitution/SKILL.md +0 -612
- package/resources/gofer/agents-skills/gofer_diagnose/SKILL.md +0 -114
- package/resources/gofer/agents-skills/gofer_eai_first_run/SKILL.md +0 -397
- package/resources/gofer/agents-skills/gofer_hydrate/SKILL.md +0 -561
- package/resources/gofer/agents-skills/gofer_personality/SKILL.md +0 -44
- package/resources/gofer/agents-skills/gofer_plan/SKILL.md +0 -40
- package/resources/gofer/agents-skills/gofer_side/SKILL.md +0 -45
- package/resources/gofer/agents-skills/gofer_spec_summary/SKILL.md +0 -112
- package/resources/gofer/agents-skills/gofer_tdd/SKILL.md +0 -112
- package/resources/gofer/agents-skills/gofer_vocabulary/SKILL.md +0 -112
- package/resources/gofer/agents-skills/gofer_zoom_out/SKILL.md +0 -112
- package/resources/gofer/claude-commands/0_gofer_start.md +0 -1282
- package/resources/gofer/claude-commands/0a_problem_validation.md +0 -424
- package/resources/gofer/claude-commands/10_gofer_cloud.md +0 -630
- package/resources/gofer/claude-commands/1_gofer_research.md +0 -1154
- package/resources/gofer/claude-commands/2_gofer_specify.md +0 -855
- package/resources/gofer/claude-commands/3_gofer_plan.md +0 -894
- package/resources/gofer/claude-commands/4_gofer_tasks.md +0 -755
- package/resources/gofer/claude-commands/5_gofer_implement.md +0 -939
- package/resources/gofer/claude-commands/6_gofer_validate.md +0 -2297
- package/resources/gofer/claude-commands/7_gofer_save.md +0 -418
- package/resources/gofer/claude-commands/7a_stakeholder_comms.md +0 -503
- package/resources/gofer/claude-commands/8_gofer_branding.md +0 -310
- package/resources/gofer/claude-commands/9_gofer_tests.md +0 -553
- package/resources/gofer/claude-commands/gofer_bootstrap_workspace.md +0 -143
- package/resources/gofer/claude-commands/gofer_check_workspace.md +0 -114
- package/resources/gofer/claude-commands/gofer_constitution.md +0 -607
- package/resources/gofer/claude-commands/gofer_diagnose.md +0 -109
- package/resources/gofer/claude-commands/gofer_eai_first_run.md +0 -392
- package/resources/gofer/claude-commands/gofer_hydrate.md +0 -556
- package/resources/gofer/claude-commands/gofer_personality.md +0 -39
- package/resources/gofer/claude-commands/gofer_plan.md +0 -35
- package/resources/gofer/claude-commands/gofer_side.md +0 -40
- package/resources/gofer/claude-commands/gofer_spec_summary.md +0 -107
- package/resources/gofer/claude-commands/gofer_tdd.md +0 -107
- package/resources/gofer/claude-commands/gofer_vocabulary.md +0 -107
- package/resources/gofer/claude-commands/gofer_zoom_out.md +0 -107
- package/resources/gofer/claude-skills/eai-gofer/SKILL.md +0 -56
- package/resources/gofer/copilot-prompts/0_gofer_start.prompt.md +0 -1308
- package/resources/gofer/copilot-prompts/0a_problem_validation.prompt.md +0 -429
- package/resources/gofer/copilot-prompts/10_gofer_cloud.prompt.md +0 -644
- package/resources/gofer/copilot-prompts/1_gofer_research.prompt.md +0 -1177
- package/resources/gofer/copilot-prompts/2_gofer_specify.prompt.md +0 -878
- package/resources/gofer/copilot-prompts/3_gofer_plan.prompt.md +0 -917
- package/resources/gofer/copilot-prompts/4_gofer_tasks.prompt.md +0 -778
- package/resources/gofer/copilot-prompts/5_gofer_implement.prompt.md +0 -962
- package/resources/gofer/copilot-prompts/6_gofer_validate.prompt.md +0 -2310
- package/resources/gofer/copilot-prompts/7_gofer_save.prompt.md +0 -433
- package/resources/gofer/copilot-prompts/7a_stakeholder_comms.prompt.md +0 -516
- package/resources/gofer/copilot-prompts/8_gofer_branding.prompt.md +0 -323
- package/resources/gofer/copilot-prompts/9_gofer_tests.prompt.md +0 -566
- package/resources/gofer/copilot-prompts/gofer_bootstrap_workspace.prompt.md +0 -161
- package/resources/gofer/copilot-prompts/gofer_check_workspace.prompt.md +0 -132
- package/resources/gofer/copilot-prompts/gofer_constitution.prompt.md +0 -621
- package/resources/gofer/copilot-prompts/gofer_diagnose.prompt.md +0 -127
- package/resources/gofer/copilot-prompts/gofer_eai_first_run.prompt.md +0 -410
- package/resources/gofer/copilot-prompts/gofer_hydrate.prompt.md +0 -571
- package/resources/gofer/copilot-prompts/gofer_personality.prompt.md +0 -57
- package/resources/gofer/copilot-prompts/gofer_plan.prompt.md +0 -53
- package/resources/gofer/copilot-prompts/gofer_side.prompt.md +0 -58
- package/resources/gofer/copilot-prompts/gofer_spec_summary.prompt.md +0 -125
- package/resources/gofer/copilot-prompts/gofer_tdd.prompt.md +0 -125
- package/resources/gofer/copilot-prompts/gofer_vocabulary.prompt.md +0 -125
- package/resources/gofer/copilot-prompts/gofer_zoom_out.prompt.md +0 -125
- package/resources/gofer/gemini/commands/gofer/0_gofer_start.md +0 -1282
- package/resources/gofer/gemini/commands/gofer/0_gofer_start.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/0a_problem_validation.md +0 -424
- package/resources/gofer/gemini/commands/gofer/0a_problem_validation.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/10_gofer_cloud.md +0 -630
- package/resources/gofer/gemini/commands/gofer/10_gofer_cloud.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/1_gofer_research.md +0 -1154
- package/resources/gofer/gemini/commands/gofer/1_gofer_research.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/2_gofer_specify.md +0 -855
- package/resources/gofer/gemini/commands/gofer/2_gofer_specify.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/3_gofer_plan.md +0 -894
- package/resources/gofer/gemini/commands/gofer/3_gofer_plan.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/4_gofer_tasks.md +0 -755
- package/resources/gofer/gemini/commands/gofer/4_gofer_tasks.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/5_gofer_implement.md +0 -939
- package/resources/gofer/gemini/commands/gofer/5_gofer_implement.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/6_gofer_validate.md +0 -2297
- package/resources/gofer/gemini/commands/gofer/6_gofer_validate.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/7_gofer_save.md +0 -418
- package/resources/gofer/gemini/commands/gofer/7_gofer_save.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/7a_stakeholder_comms.md +0 -503
- package/resources/gofer/gemini/commands/gofer/7a_stakeholder_comms.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/8_gofer_branding.md +0 -310
- package/resources/gofer/gemini/commands/gofer/8_gofer_branding.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/9_gofer_tests.md +0 -553
- package/resources/gofer/gemini/commands/gofer/9_gofer_tests.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_bootstrap_workspace.md +0 -143
- package/resources/gofer/gemini/commands/gofer/gofer_bootstrap_workspace.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_check_workspace.md +0 -114
- package/resources/gofer/gemini/commands/gofer/gofer_check_workspace.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_constitution.md +0 -607
- package/resources/gofer/gemini/commands/gofer/gofer_constitution.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_diagnose.md +0 -109
- package/resources/gofer/gemini/commands/gofer/gofer_diagnose.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_eai_first_run.md +0 -392
- package/resources/gofer/gemini/commands/gofer/gofer_eai_first_run.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_hydrate.md +0 -556
- package/resources/gofer/gemini/commands/gofer/gofer_hydrate.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_personality.md +0 -39
- package/resources/gofer/gemini/commands/gofer/gofer_personality.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_plan.md +0 -35
- package/resources/gofer/gemini/commands/gofer/gofer_plan.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_side.md +0 -40
- package/resources/gofer/gemini/commands/gofer/gofer_side.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_spec_summary.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_spec_summary.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_tdd.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_tdd.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_vocabulary.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_vocabulary.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_zoom_out.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_zoom_out.toml +0 -2
- package/resources/gofer/github-skills/eai-gofer/SKILL.md +0 -56
- package/resources/gofer/system-skills/0_gofer_start/SKILL.md +0 -1287
- package/resources/gofer/system-skills/0a_problem_validation/SKILL.md +0 -429
- package/resources/gofer/system-skills/10_gofer_cloud/SKILL.md +0 -635
- package/resources/gofer/system-skills/1_gofer_research/SKILL.md +0 -1159
- package/resources/gofer/system-skills/2_gofer_specify/SKILL.md +0 -860
- package/resources/gofer/system-skills/3_gofer_plan/SKILL.md +0 -899
- package/resources/gofer/system-skills/4_gofer_tasks/SKILL.md +0 -760
- package/resources/gofer/system-skills/5_gofer_implement/SKILL.md +0 -944
- package/resources/gofer/system-skills/6_gofer_validate/SKILL.md +0 -2302
- package/resources/gofer/system-skills/7_gofer_save/SKILL.md +0 -423
- package/resources/gofer/system-skills/7a_stakeholder_comms/SKILL.md +0 -508
- package/resources/gofer/system-skills/8_gofer_branding/SKILL.md +0 -315
- package/resources/gofer/system-skills/9_gofer_tests/SKILL.md +0 -558
- package/resources/gofer/system-skills/gofer_bootstrap_workspace/SKILL.md +0 -148
- package/resources/gofer/system-skills/gofer_check_workspace/SKILL.md +0 -119
- package/resources/gofer/system-skills/gofer_constitution/SKILL.md +0 -612
- package/resources/gofer/system-skills/gofer_diagnose/SKILL.md +0 -114
- package/resources/gofer/system-skills/gofer_eai_first_run/SKILL.md +0 -397
- package/resources/gofer/system-skills/gofer_hydrate/SKILL.md +0 -561
- package/resources/gofer/system-skills/gofer_personality/SKILL.md +0 -44
- package/resources/gofer/system-skills/gofer_plan/SKILL.md +0 -40
- package/resources/gofer/system-skills/gofer_side/SKILL.md +0 -45
- package/resources/gofer/system-skills/gofer_spec_summary/SKILL.md +0 -112
- package/resources/gofer/system-skills/gofer_tdd/SKILL.md +0 -112
- package/resources/gofer/system-skills/gofer_vocabulary/SKILL.md +0 -112
- package/resources/gofer/system-skills/gofer_zoom_out/SKILL.md +0 -112
- package/resources/gofer/templates/ui-approval-template.md +0 -32
|
@@ -3,12 +3,18 @@ import { execFileSync, spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import {
|
|
5
5
|
cpSync,
|
|
6
|
+
chmodSync,
|
|
7
|
+
closeSync,
|
|
8
|
+
constants as fsConstants,
|
|
9
|
+
fstatSync,
|
|
10
|
+
openSync,
|
|
6
11
|
existsSync,
|
|
7
12
|
linkSync,
|
|
8
13
|
lstatSync,
|
|
9
14
|
mkdirSync,
|
|
10
15
|
mkdtempSync,
|
|
11
16
|
readFileSync,
|
|
17
|
+
readSync,
|
|
12
18
|
realpathSync,
|
|
13
19
|
rmSync,
|
|
14
20
|
symlinkSync,
|
|
@@ -19,13 +25,796 @@ import { tmpdir } from 'node:os';
|
|
|
19
25
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
20
26
|
import { fileURLToPath } from 'node:url';
|
|
21
27
|
import test from 'node:test';
|
|
28
|
+
import { gunzipSync } from 'node:zlib';
|
|
29
|
+
|
|
30
|
+
const canonicalWorkflowRelativePath = '.github/workflows/eai-app.yml';
|
|
31
|
+
const fixtureDirectory = 'tests/fixtures/source-unknown';
|
|
32
|
+
const canonicalDigest =
|
|
33
|
+
'e672ee440a434b9d681a73bb00b15c3a2dbb5b0561825cfd7e6abaefd892a4cb';
|
|
34
|
+
const generatedDigest =
|
|
35
|
+
'77d4951b3e6852ead73cef8b9e8901e3de774f9aca70f71cc0c932c13903ff32';
|
|
36
|
+
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
37
|
+
const runtimeAppKeyPattern = /^[a-z][a-z0-9-]{1,62}$/;
|
|
38
|
+
const runtimeSafePathSegmentPattern =
|
|
39
|
+
/^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,254}[A-Za-z0-9])?$/;
|
|
40
|
+
const controlledRoutingProofs = new WeakMap();
|
|
41
|
+
const managedReviewPath =
|
|
42
|
+
'/api/platform/generated-apps/managed-review/workflow';
|
|
43
|
+
// SECURITY: TEST Portal currently exports through AU; the additional official TEST API pairs retain collector dispatch compatibility, not live regional adoption proof.
|
|
44
|
+
const trustedRoutingPairs = [
|
|
45
|
+
[
|
|
46
|
+
'https://dev-api.au.myenterprise.ai/public',
|
|
47
|
+
'https://dev-admin-portal.myenterprise.ai',
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
'https://test-api.au.myenterprise.ai/public',
|
|
51
|
+
'https://test-admin-portal.myenterprise.ai',
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
'https://test-api.ca.myenterprise.ai/public',
|
|
55
|
+
'https://test-admin-portal.myenterprise.ai',
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
'https://test-api.eu.myenterprise.ai/public',
|
|
59
|
+
'https://test-admin-portal.myenterprise.ai',
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
'https://api.au.myenterprise.ai/public',
|
|
63
|
+
'https://admin-portal.myenterprise.ai',
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
'https://api.eu.myenterprise.ai/public',
|
|
67
|
+
'https://admin-portal.eu.myenterprise.ai',
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
'https://api.ca.myenterprise.ai/public',
|
|
71
|
+
'https://admin-portal.ca.myenterprise.ai',
|
|
72
|
+
],
|
|
73
|
+
].map(([evidence, portal]) => [evidence, portal + managedReviewPath]);
|
|
74
|
+
const record = (value) =>
|
|
75
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
76
|
+
const boundedString = (value) =>
|
|
77
|
+
typeof value === 'string' &&
|
|
78
|
+
value.length > 0 &&
|
|
79
|
+
value.length <= 1024 &&
|
|
80
|
+
!/[\r\n\0]/.test(value);
|
|
81
|
+
const safePath = (value) =>
|
|
82
|
+
boundedString(value) &&
|
|
83
|
+
!value.startsWith('/') &&
|
|
84
|
+
!value.includes('\\') &&
|
|
85
|
+
value.split('/').every((part) => part && part !== '.' && part !== '..');
|
|
86
|
+
|
|
87
|
+
function regularBytes(path) {
|
|
88
|
+
const stat = lstatSync(path);
|
|
89
|
+
assert.ok(
|
|
90
|
+
stat.isFile() &&
|
|
91
|
+
stat.nlink === 1 &&
|
|
92
|
+
!stat.isSymbolicLink() &&
|
|
93
|
+
stat.size > 0 &&
|
|
94
|
+
stat.size <= 1024 * 1024,
|
|
95
|
+
'Workflow evidence must be a bounded regular file.',
|
|
96
|
+
);
|
|
97
|
+
return readFileSync(path);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function keys(value, allowed, label) {
|
|
101
|
+
assert.ok(
|
|
102
|
+
record(value) && Object.keys(value).every((key) => allowed.includes(key)),
|
|
103
|
+
`Unsupported ${label} fields.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateCliManifest(manifest) {
|
|
108
|
+
keys(
|
|
109
|
+
manifest,
|
|
110
|
+
['schemaVersion', 'cli', 'packages', 'template', 'gofer'],
|
|
111
|
+
'CLI manifest',
|
|
112
|
+
);
|
|
113
|
+
assert.equal(manifest.schemaVersion, 1);
|
|
114
|
+
if (manifest.cli !== undefined) {
|
|
115
|
+
keys(manifest.cli, ['version'], 'CLI version');
|
|
116
|
+
assert.ok(boundedString(manifest.cli.version));
|
|
117
|
+
}
|
|
118
|
+
if (manifest.packages !== undefined) {
|
|
119
|
+
keys(
|
|
120
|
+
manifest.packages,
|
|
121
|
+
['profile', 'source', 'recordedAt'],
|
|
122
|
+
'package profile',
|
|
123
|
+
);
|
|
124
|
+
assert.ok(
|
|
125
|
+
['external', 'internal', 'hybrid'].includes(manifest.packages.profile),
|
|
126
|
+
);
|
|
127
|
+
assert.ok(
|
|
128
|
+
manifest.packages.source === undefined ||
|
|
129
|
+
['eai-packages', 'enterpriseai-packages'].includes(
|
|
130
|
+
manifest.packages.source,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
assert.ok(
|
|
134
|
+
manifest.packages.recordedAt === undefined ||
|
|
135
|
+
boundedString(manifest.packages.recordedAt),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (manifest.template !== undefined) {
|
|
139
|
+
keys(
|
|
140
|
+
manifest.template,
|
|
141
|
+
['repo', 'version', 'commit', 'displaySource', 'initializedAt'],
|
|
142
|
+
'CLI template',
|
|
143
|
+
);
|
|
144
|
+
assert.ok(Object.values(manifest.template).every(boundedString));
|
|
145
|
+
assert.ok(
|
|
146
|
+
manifest.template.commit === undefined ||
|
|
147
|
+
/^[a-f0-9]{40}$/.test(manifest.template.commit),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (manifest.gofer !== undefined) {
|
|
151
|
+
keys(
|
|
152
|
+
manifest.gofer,
|
|
153
|
+
['bundle', 'managedFiles', 'refreshedAt'],
|
|
154
|
+
'Gofer manifest',
|
|
155
|
+
);
|
|
156
|
+
assert.ok(record(manifest.gofer.managedFiles));
|
|
157
|
+
for (const [path, entry] of Object.entries(manifest.gofer.managedFiles)) {
|
|
158
|
+
keys(entry, ['sha256', 'source'], 'Gofer file');
|
|
159
|
+
assert.ok(
|
|
160
|
+
safePath(path) &&
|
|
161
|
+
/^[a-f0-9]{64}$/.test(entry.sha256) &&
|
|
162
|
+
['bundled', 'generated'].includes(entry.source),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (manifest.gofer.bundle !== undefined) {
|
|
166
|
+
keys(
|
|
167
|
+
manifest.gofer.bundle,
|
|
168
|
+
['commit', 'describe', 'syncedAt'],
|
|
169
|
+
'Gofer bundle',
|
|
170
|
+
);
|
|
171
|
+
assert.ok(Object.values(manifest.gofer.bundle).every(boundedString));
|
|
172
|
+
assert.ok(
|
|
173
|
+
manifest.gofer.bundle.commit === undefined ||
|
|
174
|
+
/^[a-f0-9]{40}$/.test(manifest.gofer.bundle.commit),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
assert.ok(
|
|
178
|
+
manifest.gofer.refreshedAt === undefined ||
|
|
179
|
+
boundedString(manifest.gofer.refreshedAt),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateGeneratedManifest(manifest, workflow) {
|
|
185
|
+
keys(
|
|
186
|
+
manifest,
|
|
187
|
+
[
|
|
188
|
+
'schemaVersion',
|
|
189
|
+
'sourceMode',
|
|
190
|
+
'appKey',
|
|
191
|
+
'appName',
|
|
192
|
+
'generatedAt',
|
|
193
|
+
'templateRepository',
|
|
194
|
+
'workflowPath',
|
|
195
|
+
'environment',
|
|
196
|
+
'codeownersPath',
|
|
197
|
+
'repositoryGuardrailsPath',
|
|
198
|
+
'requiredBeforeTenantAccess',
|
|
199
|
+
'runtimeBinding',
|
|
200
|
+
'theme',
|
|
201
|
+
'managedFiles',
|
|
202
|
+
'generatedFileScope',
|
|
203
|
+
],
|
|
204
|
+
'generated manifest',
|
|
205
|
+
);
|
|
206
|
+
assert.equal(manifest.schemaVersion, 'eai.generated_app_manifest.v1');
|
|
207
|
+
assert.equal(manifest.sourceMode, 'admin-portal-generated');
|
|
208
|
+
assert.ok(
|
|
209
|
+
typeof manifest.appKey === 'string' &&
|
|
210
|
+
runtimeAppKeyPattern.test(manifest.appKey),
|
|
211
|
+
);
|
|
212
|
+
assert.ok(
|
|
213
|
+
boundedString(manifest.appName) &&
|
|
214
|
+
boundedString(manifest.generatedAt) &&
|
|
215
|
+
Number.isFinite(Date.parse(manifest.generatedAt)),
|
|
216
|
+
);
|
|
217
|
+
assert.ok(
|
|
218
|
+
['eai-tools/eai-app-template', 'eai-support/eai-app-template'].includes(
|
|
219
|
+
manifest.templateRepository,
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
assert.equal(manifest.workflowPath, canonicalWorkflowRelativePath);
|
|
223
|
+
assert.equal(manifest.environment, 'eai-generated-preview');
|
|
224
|
+
assert.equal(manifest.codeownersPath, '.github/CODEOWNERS');
|
|
225
|
+
assert.equal(
|
|
226
|
+
manifest.repositoryGuardrailsPath,
|
|
227
|
+
'.github/eai-repository-guardrails.json',
|
|
228
|
+
);
|
|
229
|
+
assert.deepEqual(manifest.requiredBeforeTenantAccess, [
|
|
230
|
+
'branch_ruleset',
|
|
231
|
+
'workflow_path_protection',
|
|
232
|
+
'environment_protection',
|
|
233
|
+
'codeowners',
|
|
234
|
+
'publicapi_github_oidc_allowlist',
|
|
235
|
+
]);
|
|
236
|
+
assert.ok(
|
|
237
|
+
Array.isArray(manifest.managedFiles) &&
|
|
238
|
+
manifest.managedFiles.length > 0 &&
|
|
239
|
+
manifest.managedFiles.length <= 4096,
|
|
240
|
+
);
|
|
241
|
+
const paths = [];
|
|
242
|
+
for (const file of manifest.managedFiles) {
|
|
243
|
+
keys(file, ['path', 'checksum', 'encoding', 'owner'], 'generated file');
|
|
244
|
+
assert.ok(
|
|
245
|
+
safePath(file.path) && /^sha256:[a-f0-9]{64}$/.test(file.checksum),
|
|
246
|
+
);
|
|
247
|
+
assert.ok(['utf8', 'base64'].includes(file.encoding));
|
|
248
|
+
assert.equal(file.owner, 'admin-portal-generated');
|
|
249
|
+
assert.ok(!paths.includes(file.path), 'Duplicate generated file scope.');
|
|
250
|
+
paths.push(file.path);
|
|
251
|
+
}
|
|
252
|
+
assert.deepEqual(manifest.generatedFileScope, paths);
|
|
253
|
+
const declared = manifest.managedFiles.find(
|
|
254
|
+
(file) => file.path === canonicalWorkflowRelativePath,
|
|
255
|
+
);
|
|
256
|
+
assert.ok(
|
|
257
|
+
declared && declared.encoding === 'utf8',
|
|
258
|
+
'Managed workflow is missing.',
|
|
259
|
+
);
|
|
260
|
+
assert.equal(
|
|
261
|
+
declared.checksum,
|
|
262
|
+
`sha256:${digest(workflow)}`,
|
|
263
|
+
'Exported workflow checksum does not match its manifest.',
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function quotedBinding(workflow, name) {
|
|
268
|
+
const pattern = new RegExp(`^ ${name}: ("[^\\r\\n]*")$`, 'm');
|
|
269
|
+
const match = pattern.exec(workflow);
|
|
270
|
+
assert.ok(match, `Missing protected ${name}.`);
|
|
271
|
+
const value = JSON.parse(match[1]);
|
|
272
|
+
assert.ok(boundedString(value), `Malformed protected ${name}.`);
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function secureUrl(value, exactPath) {
|
|
277
|
+
assert.ok(boundedString(value));
|
|
278
|
+
const parsed = new URL(value);
|
|
279
|
+
assert.ok(
|
|
280
|
+
parsed.protocol === 'https:' &&
|
|
281
|
+
!parsed.username &&
|
|
282
|
+
!parsed.password &&
|
|
283
|
+
!parsed.search &&
|
|
284
|
+
!parsed.hash &&
|
|
285
|
+
!parsed.port &&
|
|
286
|
+
!value.slice('https://'.length).split('/')[0].includes(':'),
|
|
287
|
+
'Workflow destination must be credential-free HTTPS without an explicit port.',
|
|
288
|
+
);
|
|
289
|
+
if (exactPath)
|
|
290
|
+
assert.equal(
|
|
291
|
+
parsed.pathname,
|
|
292
|
+
exactPath,
|
|
293
|
+
'Wrong managed-review destination.',
|
|
294
|
+
);
|
|
295
|
+
return parsed;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function controlledReadFlags(constants = fsConstants) {
|
|
299
|
+
for (const name of ['O_NOFOLLOW', 'O_NONBLOCK']) {
|
|
300
|
+
assert.ok(
|
|
301
|
+
Number.isSafeInteger(constants[name]) && constants[name] > 0,
|
|
302
|
+
`Controlled secure reads require ${name} support.`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
assert.ok(
|
|
306
|
+
Number.isSafeInteger(constants.O_RDONLY) && constants.O_RDONLY >= 0,
|
|
307
|
+
'Controlled secure reads require O_RDONLY support.',
|
|
308
|
+
);
|
|
309
|
+
return constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function snapshotControlledDirectories(directory) {
|
|
313
|
+
const identities = [];
|
|
314
|
+
let current = resolve(directory);
|
|
315
|
+
while (true) {
|
|
316
|
+
const status = lstatSync(current);
|
|
317
|
+
assert.ok(
|
|
318
|
+
status.isDirectory() && !status.isSymbolicLink(),
|
|
319
|
+
'Controlled input ancestors must remain no-follow directories.',
|
|
320
|
+
);
|
|
321
|
+
identities.push({
|
|
322
|
+
path: current,
|
|
323
|
+
dev: status.dev,
|
|
324
|
+
ino: status.ino,
|
|
325
|
+
uid: status.uid,
|
|
326
|
+
mode: status.mode,
|
|
327
|
+
});
|
|
328
|
+
const parent = dirname(current);
|
|
329
|
+
if (parent === current) break;
|
|
330
|
+
current = parent;
|
|
331
|
+
}
|
|
332
|
+
return identities;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function assertControlledDirectories(identities) {
|
|
336
|
+
for (const identity of identities) {
|
|
337
|
+
const status = lstatSync(identity.path);
|
|
338
|
+
assert.ok(
|
|
339
|
+
status.isDirectory() &&
|
|
340
|
+
!status.isSymbolicLink() &&
|
|
341
|
+
status.dev === identity.dev &&
|
|
342
|
+
status.ino === identity.ino &&
|
|
343
|
+
status.uid === identity.uid &&
|
|
344
|
+
status.mode === identity.mode,
|
|
345
|
+
'Controlled input parent chain changed during its bound read.',
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function controlledFileMatches(actual, expected) {
|
|
351
|
+
return (
|
|
352
|
+
actual.isFile() &&
|
|
353
|
+
!actual.isSymbolicLink() &&
|
|
354
|
+
actual.nlink === 1 &&
|
|
355
|
+
actual.dev === expected.dev &&
|
|
356
|
+
actual.ino === expected.ino &&
|
|
357
|
+
actual.uid === expected.uid &&
|
|
358
|
+
actual.mode === expected.mode &&
|
|
359
|
+
actual.size === expected.size &&
|
|
360
|
+
actual.mtimeMs === expected.mtimeMs &&
|
|
361
|
+
actual.ctimeMs === expected.ctimeMs
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** SECURITY: the explicit local override is owner-run controlled qualification only, never repository-derived destination trust or live authorization. */
|
|
366
|
+
function readProtectedRoutingBytes(inputPath, candidateRoot, maxBytes = 8192) {
|
|
367
|
+
assert.ok(
|
|
368
|
+
process.platform !== 'win32' && typeof process.getuid === 'function',
|
|
369
|
+
'Controlled local routing requires POSIX owner protection.',
|
|
370
|
+
);
|
|
371
|
+
const flags = controlledReadFlags();
|
|
372
|
+
assert.ok(
|
|
373
|
+
Number.isSafeInteger(maxBytes) && maxBytes > 0 && maxBytes <= 512 * 1024,
|
|
374
|
+
'Controlled input byte limit is invalid.',
|
|
375
|
+
);
|
|
376
|
+
assert.ok(
|
|
377
|
+
isAbsolute(inputPath),
|
|
378
|
+
'Controlled routing input must be an absolute external path.',
|
|
379
|
+
);
|
|
380
|
+
const path = resolve(inputPath);
|
|
381
|
+
assert.equal(
|
|
382
|
+
realpathSync(path),
|
|
383
|
+
path,
|
|
384
|
+
'Controlled routing input must not traverse symlinks.',
|
|
385
|
+
);
|
|
386
|
+
const root = realpathSync(candidateRoot);
|
|
387
|
+
const within = relative(root, path);
|
|
388
|
+
assert.ok(
|
|
389
|
+
within === '..' || within.startsWith('../') || isAbsolute(within),
|
|
390
|
+
'Controlled routing input must be outside the candidate repository.',
|
|
391
|
+
);
|
|
392
|
+
const ancestors = snapshotControlledDirectories(dirname(path));
|
|
393
|
+
const parent = ancestors[0];
|
|
394
|
+
assert.ok(
|
|
395
|
+
parent.uid === process.getuid() && (parent.mode & 0o777) === 0o700,
|
|
396
|
+
'Controlled routing parent must be owner-protected 0700.',
|
|
397
|
+
);
|
|
398
|
+
const before = lstatSync(path);
|
|
399
|
+
const fd = openSync(path, flags);
|
|
400
|
+
try {
|
|
401
|
+
const opened = fstatSync(fd);
|
|
402
|
+
assertControlledDirectories(ancestors);
|
|
403
|
+
assert.ok(
|
|
404
|
+
opened.isFile() &&
|
|
405
|
+
opened.nlink === 1 &&
|
|
406
|
+
opened.uid === process.getuid() &&
|
|
407
|
+
(opened.mode & 0o777) === 0o600 &&
|
|
408
|
+
opened.size > 0 &&
|
|
409
|
+
opened.size <= maxBytes,
|
|
410
|
+
'Controlled routing input must be a bounded owner-only single-link file.',
|
|
411
|
+
);
|
|
412
|
+
assert.ok(
|
|
413
|
+
controlledFileMatches(opened, before) &&
|
|
414
|
+
controlledFileMatches(lstatSync(path), opened),
|
|
415
|
+
'Controlled routing leaf changed before its bound read.',
|
|
416
|
+
);
|
|
417
|
+
const bytes = Buffer.alloc(opened.size);
|
|
418
|
+
let offset = 0;
|
|
419
|
+
while (offset < bytes.length) {
|
|
420
|
+
const count = readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
421
|
+
assert.ok(
|
|
422
|
+
count > 0,
|
|
423
|
+
'Controlled routing input shrank during its bounded read.',
|
|
424
|
+
);
|
|
425
|
+
offset += count;
|
|
426
|
+
}
|
|
427
|
+
assert.equal(
|
|
428
|
+
readSync(fd, Buffer.alloc(1), 0, 1, offset),
|
|
429
|
+
0,
|
|
430
|
+
'Controlled routing input grew during its bounded read.',
|
|
431
|
+
);
|
|
432
|
+
const after = fstatSync(fd);
|
|
433
|
+
assertControlledDirectories(ancestors);
|
|
434
|
+
assert.ok(
|
|
435
|
+
controlledFileMatches(after, opened) &&
|
|
436
|
+
controlledFileMatches(lstatSync(path), opened),
|
|
437
|
+
'Controlled routing bytes changed during the protected read.',
|
|
438
|
+
);
|
|
439
|
+
return bytes;
|
|
440
|
+
} finally {
|
|
441
|
+
closeSync(fd);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function validateControlledConfigReceipts(proof, configPaths, candidateRoot) {
|
|
446
|
+
keys(
|
|
447
|
+
configPaths,
|
|
448
|
+
['publicApi', 'portal'],
|
|
449
|
+
'explicit independent source-config paths',
|
|
450
|
+
);
|
|
451
|
+
assert.deepEqual(Object.keys(configPaths).sort(), ['portal', 'publicApi']);
|
|
452
|
+
for (const name of ['publicApi', 'portal']) {
|
|
453
|
+
assert.ok(
|
|
454
|
+
boundedString(configPaths[name]),
|
|
455
|
+
'Controlled qualification requires explicit independent source-config paths.',
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
assert.notEqual(
|
|
459
|
+
resolve(configPaths.publicApi),
|
|
460
|
+
resolve(configPaths.portal),
|
|
461
|
+
'Independent source-config receipts require distinct resolved paths.',
|
|
462
|
+
);
|
|
463
|
+
for (const name of ['publicApi', 'portal']) {
|
|
464
|
+
const bytes = readProtectedRoutingBytes(
|
|
465
|
+
configPaths[name],
|
|
466
|
+
candidateRoot,
|
|
467
|
+
512 * 1024,
|
|
468
|
+
);
|
|
469
|
+
assert.equal(
|
|
470
|
+
digest(bytes),
|
|
471
|
+
proof.sourceConfigSha256[name],
|
|
472
|
+
'Independent source-config receipt digest does not match.',
|
|
473
|
+
);
|
|
474
|
+
let config;
|
|
475
|
+
try {
|
|
476
|
+
config = JSON.parse(bytes.toString('utf8'));
|
|
477
|
+
assert.ok(record(config));
|
|
478
|
+
const evidence = secureUrl(config.ADMIN_PORTAL_PUBLIC_API_BASE_URL);
|
|
479
|
+
const callback = secureUrl(
|
|
480
|
+
config.ADMIN_PORTAL_WORKFLOW_CALLBACK_BASE_URL,
|
|
481
|
+
);
|
|
482
|
+
assert.equal(callback.pathname, '/');
|
|
483
|
+
assert.ok(['/', '/public', '/public/'].includes(evidence.pathname));
|
|
484
|
+
} catch {
|
|
485
|
+
throw new Error(
|
|
486
|
+
'Independent source-config receipt has invalid preferred routing fields.',
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
assert.equal(
|
|
490
|
+
config.ADMIN_PORTAL_PUBLIC_API_BASE_URL,
|
|
491
|
+
proof.evidenceBaseUrl,
|
|
492
|
+
'Independent evidence destination does not match.',
|
|
493
|
+
);
|
|
494
|
+
assert.equal(
|
|
495
|
+
config.ADMIN_PORTAL_WORKFLOW_CALLBACK_BASE_URL.replace(/\/$/, '') +
|
|
496
|
+
managedReviewPath,
|
|
497
|
+
proof.managedReviewCallbackUrl,
|
|
498
|
+
'Independent callback destination does not match.',
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function readControlledRoutingProof(inputPath, candidateRoot, configPaths) {
|
|
504
|
+
const bytes = readProtectedRoutingBytes(inputPath, candidateRoot);
|
|
505
|
+
const proof = JSON.parse(bytes.toString('utf8'));
|
|
506
|
+
keys(
|
|
507
|
+
proof,
|
|
508
|
+
[
|
|
509
|
+
'schemaVersion',
|
|
510
|
+
'mode',
|
|
511
|
+
'appKey',
|
|
512
|
+
'tenantId',
|
|
513
|
+
'operationId',
|
|
514
|
+
'workflowSha256',
|
|
515
|
+
'generatedOperationSha256',
|
|
516
|
+
'evidenceBaseUrl',
|
|
517
|
+
'managedReviewCallbackUrl',
|
|
518
|
+
'sourceConfigSha256',
|
|
519
|
+
],
|
|
520
|
+
'controlled routing input',
|
|
521
|
+
);
|
|
522
|
+
assert.equal(
|
|
523
|
+
bytes.toString('utf8'),
|
|
524
|
+
JSON.stringify(proof, null, 2) + '\n',
|
|
525
|
+
'Controlled routing input must retain canonical owner-written JSON.',
|
|
526
|
+
);
|
|
527
|
+
assert.equal(proof.schemaVersion, 'eai.template_controlled_routing.v1');
|
|
528
|
+
assert.equal(proof.mode, 'owner-controlled-qualification-only');
|
|
529
|
+
assert.ok(
|
|
530
|
+
runtimeAppKeyPattern.test(proof.appKey) &&
|
|
531
|
+
boundedString(proof.tenantId) &&
|
|
532
|
+
boundedString(proof.operationId),
|
|
533
|
+
);
|
|
534
|
+
for (const field of ['workflowSha256', 'generatedOperationSha256'])
|
|
535
|
+
assert.ok(/^[a-f0-9]{64}$/.test(proof[field]));
|
|
536
|
+
keys(
|
|
537
|
+
proof.sourceConfigSha256,
|
|
538
|
+
['publicApi', 'portal'],
|
|
539
|
+
'independent source-config receipt digests',
|
|
540
|
+
);
|
|
541
|
+
assert.deepEqual(Object.keys(proof.sourceConfigSha256).sort(), [
|
|
542
|
+
'portal',
|
|
543
|
+
'publicApi',
|
|
544
|
+
]);
|
|
545
|
+
assert.ok(
|
|
546
|
+
Object.values(proof.sourceConfigSha256).every((x) =>
|
|
547
|
+
/^[a-f0-9]{64}$/.test(x),
|
|
548
|
+
),
|
|
549
|
+
);
|
|
550
|
+
const evidence = secureUrl(proof.evidenceBaseUrl);
|
|
551
|
+
const callback = secureUrl(proof.managedReviewCallbackUrl, managedReviewPath);
|
|
552
|
+
assert.ok(
|
|
553
|
+
/^[a-z0-9-]+-8000\.[a-z0-9-]+\.devtunnels\.ms$/.test(evidence.hostname) &&
|
|
554
|
+
['/', '/public', '/public/'].includes(evidence.pathname),
|
|
555
|
+
'Controlled evidence destination must be the local port-8000 HTTPS tunnel.',
|
|
556
|
+
);
|
|
557
|
+
assert.ok(
|
|
558
|
+
/^[a-z0-9-]+-3010\.[a-z0-9-]+\.devtunnels\.ms$/.test(callback.hostname),
|
|
559
|
+
'Controlled callback must be the local port-3010 HTTPS tunnel.',
|
|
560
|
+
);
|
|
561
|
+
validateControlledConfigReceipts(proof, configPaths, candidateRoot);
|
|
562
|
+
const handle = Object.freeze({});
|
|
563
|
+
controlledRoutingProofs.set(handle, {
|
|
564
|
+
proof,
|
|
565
|
+
path: inputPath,
|
|
566
|
+
candidateRoot,
|
|
567
|
+
configPaths: Object.freeze({ ...configPaths }),
|
|
568
|
+
bytesSha256: digest(bytes),
|
|
569
|
+
});
|
|
570
|
+
return handle;
|
|
571
|
+
}
|
|
22
572
|
|
|
573
|
+
function validateTrustedRouting(
|
|
574
|
+
evidenceUrl,
|
|
575
|
+
callbackUrl,
|
|
576
|
+
manifest,
|
|
577
|
+
workflowBytes,
|
|
578
|
+
operation,
|
|
579
|
+
operationBytes,
|
|
580
|
+
controlledProof,
|
|
581
|
+
) {
|
|
582
|
+
secureUrl(evidenceUrl);
|
|
583
|
+
secureUrl(callbackUrl, managedReviewPath);
|
|
584
|
+
if (controlledProof !== undefined) {
|
|
585
|
+
const captured = controlledRoutingProofs.get(controlledProof);
|
|
586
|
+
assert.ok(captured, 'Unrecognized controlled routing proof.');
|
|
587
|
+
assert.equal(
|
|
588
|
+
digest(readProtectedRoutingBytes(captured.path, captured.candidateRoot)),
|
|
589
|
+
captured.bytesSha256,
|
|
590
|
+
'Controlled routing proof changed after capture.',
|
|
591
|
+
);
|
|
592
|
+
const proof = captured.proof;
|
|
593
|
+
validateControlledConfigReceipts(
|
|
594
|
+
proof,
|
|
595
|
+
captured.configPaths,
|
|
596
|
+
captured.candidateRoot,
|
|
597
|
+
);
|
|
598
|
+
assert.equal(proof.appKey, manifest.appKey);
|
|
599
|
+
assert.equal(proof.tenantId, operation.tenantId);
|
|
600
|
+
assert.equal(proof.operationId, operation.operationId);
|
|
601
|
+
assert.equal(proof.workflowSha256, digest(workflowBytes));
|
|
602
|
+
assert.ok(
|
|
603
|
+
Buffer.isBuffer(operationBytes),
|
|
604
|
+
'Controlled routing requires raw generated-operation bytes.',
|
|
605
|
+
);
|
|
606
|
+
assert.equal(proof.generatedOperationSha256, digest(operationBytes));
|
|
607
|
+
assert.equal(proof.evidenceBaseUrl, evidenceUrl);
|
|
608
|
+
assert.equal(proof.managedReviewCallbackUrl, callbackUrl);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const normalizedEvidence = evidenceUrl.replace(/\/$/, '');
|
|
612
|
+
assert.ok(
|
|
613
|
+
trustedRoutingPairs.some(
|
|
614
|
+
([evidence, callback]) =>
|
|
615
|
+
evidence === normalizedEvidence && callback === callbackUrl,
|
|
616
|
+
),
|
|
617
|
+
'Generated workflow destinations must match one exact trusted Portal cell pair.',
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** SECURITY: only declared generator inputs may vary; every remaining workflow byte must retain the reviewed OIDC, validation and handoff contract. */
|
|
622
|
+
function validateGeneratedWorkflow(
|
|
623
|
+
manifest,
|
|
624
|
+
workflowBytes,
|
|
625
|
+
fixtureBytes,
|
|
626
|
+
operation,
|
|
627
|
+
controlledProof,
|
|
628
|
+
operationBytes,
|
|
629
|
+
) {
|
|
630
|
+
assert.equal(
|
|
631
|
+
digest(fixtureBytes),
|
|
632
|
+
generatedDigest,
|
|
633
|
+
'Generated workflow contract fixture drifted.',
|
|
634
|
+
);
|
|
635
|
+
validateGeneratedManifest(manifest, workflowBytes);
|
|
636
|
+
keys(
|
|
637
|
+
operation,
|
|
638
|
+
[
|
|
639
|
+
'appKey',
|
|
640
|
+
'configHash',
|
|
641
|
+
'githubInstallationId',
|
|
642
|
+
'operationId',
|
|
643
|
+
'schemaVersion',
|
|
644
|
+
'tenantId',
|
|
645
|
+
],
|
|
646
|
+
'generated source operation',
|
|
647
|
+
);
|
|
648
|
+
assert.equal(operation.schemaVersion, 'eai.generated_source_operation.v1');
|
|
649
|
+
assert.ok(runtimeAppKeyPattern.test(operation.appKey));
|
|
650
|
+
assert.equal(operation.appKey, manifest.appKey);
|
|
651
|
+
assert.ok(/^sha256:[a-f0-9]{64}$/.test(operation.configHash));
|
|
652
|
+
assert.ok(
|
|
653
|
+
typeof operation.githubInstallationId === 'string' &&
|
|
654
|
+
/^[1-9][0-9]*$/.test(operation.githubInstallationId),
|
|
655
|
+
);
|
|
656
|
+
assert.ok(
|
|
657
|
+
boundedString(operation.operationId) &&
|
|
658
|
+
runtimeSafePathSegmentPattern.test(operation.operationId) &&
|
|
659
|
+
boundedString(operation.tenantId) &&
|
|
660
|
+
runtimeSafePathSegmentPattern.test(operation.tenantId),
|
|
661
|
+
'Generated operation and tenant IDs must match collector safe path grammar.',
|
|
662
|
+
);
|
|
663
|
+
let workflow = workflowBytes.toString('utf8');
|
|
664
|
+
const branchMatch = /^ - ("[^\r\n]*")$/m.exec(workflow);
|
|
665
|
+
assert.ok(branchMatch, 'Missing protected default branch.');
|
|
666
|
+
const branch = JSON.parse(branchMatch[1]);
|
|
667
|
+
assert.ok(
|
|
668
|
+
typeof branch === 'string' &&
|
|
669
|
+
/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/.test(branch) &&
|
|
670
|
+
!branch.includes('..'),
|
|
671
|
+
);
|
|
672
|
+
const ownerMatch =
|
|
673
|
+
/github\.actor == '([A-Za-z0-9-]+)\[bot\]' && github\.repository_owner == '\1'/.exec(
|
|
674
|
+
workflow,
|
|
675
|
+
);
|
|
676
|
+
assert.ok(
|
|
677
|
+
ownerMatch,
|
|
678
|
+
'Managed review must bind one exact repository owner and bot.',
|
|
679
|
+
);
|
|
680
|
+
const target = quotedBinding(workflow, 'EAI_MANAGED_REVIEW_TARGET_TENANT_ID');
|
|
681
|
+
assert.equal(
|
|
682
|
+
target,
|
|
683
|
+
operation.tenantId,
|
|
684
|
+
'Workflow target must match the generated operation tenant.',
|
|
685
|
+
);
|
|
686
|
+
const evidenceUrl = quotedBinding(workflow, 'EAI_WORKFLOW_EVIDENCE_BASE_URL');
|
|
687
|
+
const callbackUrl = quotedBinding(
|
|
688
|
+
workflow,
|
|
689
|
+
'EAI_MANAGED_REVIEW_CALLBACK_URL',
|
|
690
|
+
);
|
|
691
|
+
validateTrustedRouting(
|
|
692
|
+
evidenceUrl,
|
|
693
|
+
callbackUrl,
|
|
694
|
+
manifest,
|
|
695
|
+
workflowBytes,
|
|
696
|
+
operation,
|
|
697
|
+
operationBytes,
|
|
698
|
+
controlledProof,
|
|
699
|
+
);
|
|
700
|
+
assert.equal(
|
|
701
|
+
quotedBinding(workflow, 'EAI_GITHUB_OIDC_AUDIENCE'),
|
|
702
|
+
'api://enterprise-ai-publicapi/generated-app',
|
|
703
|
+
);
|
|
704
|
+
const replacements = [
|
|
705
|
+
[` - ${JSON.stringify(branch)}`, ' - "__DEFAULT_BRANCH__"'],
|
|
706
|
+
[
|
|
707
|
+
`event.repository?.default_branch !== ${JSON.stringify(branch)}`,
|
|
708
|
+
'event.repository?.default_branch !== "__DEFAULT_BRANCH__"',
|
|
709
|
+
],
|
|
710
|
+
[
|
|
711
|
+
`pr.base?.ref === ${JSON.stringify(branch)}`,
|
|
712
|
+
'pr.base?.ref === "__DEFAULT_BRANCH__"',
|
|
713
|
+
],
|
|
714
|
+
[`'refs/heads/${branch}'`, "'refs/heads/__DEFAULT_BRANCH__'"],
|
|
715
|
+
[
|
|
716
|
+
`workflowHeadBranch: '${branch}'`,
|
|
717
|
+
"workflowHeadBranch: '__DEFAULT_BRANCH__'",
|
|
718
|
+
],
|
|
719
|
+
[JSON.stringify(target), '"__TARGET_TENANT_ID__"'],
|
|
720
|
+
[
|
|
721
|
+
`EAI_APP_KEY: ${JSON.stringify(manifest.appKey)}`,
|
|
722
|
+
'EAI_APP_KEY: "__APP_KEY__"',
|
|
723
|
+
],
|
|
724
|
+
[
|
|
725
|
+
JSON.stringify(`src/eai.config/tenants/${manifest.appKey}.config.ts`),
|
|
726
|
+
'"src/eai.config/tenants/__APP_KEY__.config.ts"',
|
|
727
|
+
],
|
|
728
|
+
[`'${ownerMatch[1]}[bot]'`, "'__MANAGED_REVIEW_OWNER__[bot]'"],
|
|
729
|
+
[`'${ownerMatch[1]}'`, "'__MANAGED_REVIEW_OWNER__'"],
|
|
730
|
+
[JSON.stringify(evidenceUrl), '"__EVIDENCE_BASE_URL__"'],
|
|
731
|
+
[JSON.stringify(callbackUrl), '"__MANAGED_REVIEW_CALLBACK_URL__"'],
|
|
732
|
+
];
|
|
733
|
+
for (const [value, placeholder] of replacements)
|
|
734
|
+
workflow = workflow.replaceAll(value, placeholder);
|
|
735
|
+
assert.ok(
|
|
736
|
+
workflow === fixtureBytes.toString('utf8'),
|
|
737
|
+
'Exported workflow changed its reviewed validation, OIDC or handoff contract.',
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** INVARIANT: exported NCB workflows are validated separately; the original collector controls always run against exact canonical workflow bytes. */
|
|
742
|
+
function resolveDeploymentWorkflowContext(
|
|
743
|
+
root,
|
|
744
|
+
{ controlledRoutingInputPath, controlledRoutingConfigPaths } = {},
|
|
745
|
+
) {
|
|
746
|
+
const actual = regularBytes(join(root, canonicalWorkflowRelativePath));
|
|
747
|
+
const manifestPath = join(root, '.eai-manifest.json');
|
|
748
|
+
if (!existsSync(manifestPath)) {
|
|
749
|
+
assert.equal(
|
|
750
|
+
digest(actual),
|
|
751
|
+
canonicalDigest,
|
|
752
|
+
'Template workflow must retain the pinned canonical digest.',
|
|
753
|
+
);
|
|
754
|
+
return {
|
|
755
|
+
context: 'template',
|
|
756
|
+
workflowPath: join(root, canonicalWorkflowRelativePath),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const manifest = JSON.parse(regularBytes(manifestPath));
|
|
760
|
+
if (manifest.schemaVersion === 1) {
|
|
761
|
+
validateCliManifest(manifest);
|
|
762
|
+
assert.equal(
|
|
763
|
+
digest(actual),
|
|
764
|
+
canonicalDigest,
|
|
765
|
+
'CLI workflow must retain the pinned canonical digest.',
|
|
766
|
+
);
|
|
767
|
+
return {
|
|
768
|
+
context: 'cli',
|
|
769
|
+
workflowPath: join(root, canonicalWorkflowRelativePath),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
assert.equal(
|
|
773
|
+
manifest.schemaVersion,
|
|
774
|
+
'eai.generated_app_manifest.v1',
|
|
775
|
+
'Unsupported deployment workflow context.',
|
|
776
|
+
);
|
|
777
|
+
const canonicalPath = join(root, fixtureDirectory, 'eai-app.yml');
|
|
778
|
+
assert.equal(
|
|
779
|
+
digest(regularBytes(canonicalPath)),
|
|
780
|
+
canonicalDigest,
|
|
781
|
+
'Canonical source-unknown workflow fixture drifted.',
|
|
782
|
+
);
|
|
783
|
+
const operationBytes = regularBytes(
|
|
784
|
+
join(root, '.eai/generated-source-operation.json'),
|
|
785
|
+
);
|
|
786
|
+
const operation = JSON.parse(operationBytes);
|
|
787
|
+
const controlledProof =
|
|
788
|
+
controlledRoutingInputPath === undefined
|
|
789
|
+
? undefined
|
|
790
|
+
: readControlledRoutingProof(
|
|
791
|
+
controlledRoutingInputPath,
|
|
792
|
+
root,
|
|
793
|
+
controlledRoutingConfigPaths,
|
|
794
|
+
);
|
|
795
|
+
validateGeneratedWorkflow(
|
|
796
|
+
manifest,
|
|
797
|
+
actual,
|
|
798
|
+
regularBytes(join(root, fixtureDirectory, 'generated-eai-app.yml')),
|
|
799
|
+
operation,
|
|
800
|
+
controlledProof,
|
|
801
|
+
operationBytes,
|
|
802
|
+
);
|
|
803
|
+
return { context: 'admin-portal-generated', workflowPath: canonicalPath };
|
|
804
|
+
}
|
|
23
805
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
806
|
const evidenceScript = join(
|
|
25
807
|
repoRoot,
|
|
26
808
|
'scripts/source-unknown-deployment-evidence.mjs',
|
|
27
809
|
);
|
|
28
|
-
const
|
|
810
|
+
const selectedControlledRoutingConfigPaths = {
|
|
811
|
+
publicApi: process.env.EAI_TEMPLATE_CONTROLLED_PUBLICAPI_CONFIG,
|
|
812
|
+
portal: process.env.EAI_TEMPLATE_CONTROLLED_PORTAL_CONFIG,
|
|
813
|
+
};
|
|
814
|
+
const { workflowPath } = resolveDeploymentWorkflowContext(repoRoot, {
|
|
815
|
+
controlledRoutingInputPath: process.env.EAI_TEMPLATE_CONTROLLED_ROUTING_INPUT,
|
|
816
|
+
controlledRoutingConfigPaths: selectedControlledRoutingConfigPaths,
|
|
817
|
+
});
|
|
29
818
|
const readmePath = join(repoRoot, 'README.md');
|
|
30
819
|
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
|
31
820
|
|
|
@@ -4692,3 +5481,1354 @@ syncBuiltinESMExports();
|
|
|
4692
5481
|
rmSync(workDir, { recursive: true, force: true });
|
|
4693
5482
|
}
|
|
4694
5483
|
});
|
|
5484
|
+
|
|
5485
|
+
const root = repoRoot;
|
|
5486
|
+
const fixtures = join(root, 'tests/fixtures/source-unknown');
|
|
5487
|
+
const canonical = regularBytes(workflowPath);
|
|
5488
|
+
// INVARIANT: synthetic controlled input stays inline so legacy four-file repairs execute every control without new fixture dependencies.
|
|
5489
|
+
const generated = gunzipSync(
|
|
5490
|
+
Buffer.from(
|
|
5491
|
+
'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',
|
|
5492
|
+
'base64',
|
|
5493
|
+
),
|
|
5494
|
+
);
|
|
5495
|
+
assert.equal(digest(generated), generatedDigest);
|
|
5496
|
+
|
|
5497
|
+
const checksum = (bytes) =>
|
|
5498
|
+
`sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
5499
|
+
|
|
5500
|
+
function exportedFixture(overrides = {}) {
|
|
5501
|
+
const values = {
|
|
5502
|
+
DEFAULT_BRANCH: 'main',
|
|
5503
|
+
TARGET_TENANT_ID: 'customer-tenant-1',
|
|
5504
|
+
APP_KEY: 'incident-intake',
|
|
5505
|
+
MANAGED_REVIEW_OWNER: 'eai-generated-apps',
|
|
5506
|
+
EVIDENCE_BASE_URL: 'https://dev-api.au.myenterprise.ai/public',
|
|
5507
|
+
MANAGED_REVIEW_CALLBACK_URL:
|
|
5508
|
+
'https://dev-admin-portal.myenterprise.ai/api/platform/generated-apps/managed-review/workflow',
|
|
5509
|
+
...overrides,
|
|
5510
|
+
};
|
|
5511
|
+
let workflow = generated.toString('utf8');
|
|
5512
|
+
for (const [name, value] of Object.entries(values))
|
|
5513
|
+
workflow = workflow.replaceAll(`__${name}__`, value);
|
|
5514
|
+
const operation = {
|
|
5515
|
+
schemaVersion: 'eai.generated_source_operation.v1',
|
|
5516
|
+
appKey: values.APP_KEY,
|
|
5517
|
+
tenantId: values.TARGET_TENANT_ID,
|
|
5518
|
+
operationId: 'source-123',
|
|
5519
|
+
configHash: `sha256:${'a'.repeat(64)}`,
|
|
5520
|
+
githubInstallationId: '123',
|
|
5521
|
+
};
|
|
5522
|
+
const manifest = {
|
|
5523
|
+
schemaVersion: 'eai.generated_app_manifest.v1',
|
|
5524
|
+
sourceMode: 'admin-portal-generated',
|
|
5525
|
+
appKey: values.APP_KEY,
|
|
5526
|
+
appName: 'Incident intake',
|
|
5527
|
+
generatedAt: '2026-10-08T05:42:52.888Z',
|
|
5528
|
+
templateRepository: 'eai-tools/eai-app-template',
|
|
5529
|
+
workflowPath: canonicalWorkflowRelativePath,
|
|
5530
|
+
environment: 'eai-generated-preview',
|
|
5531
|
+
codeownersPath: '.github/CODEOWNERS',
|
|
5532
|
+
repositoryGuardrailsPath: '.github/eai-repository-guardrails.json',
|
|
5533
|
+
requiredBeforeTenantAccess: [
|
|
5534
|
+
'branch_ruleset',
|
|
5535
|
+
'workflow_path_protection',
|
|
5536
|
+
'environment_protection',
|
|
5537
|
+
'codeowners',
|
|
5538
|
+
'publicapi_github_oidc_allowlist',
|
|
5539
|
+
],
|
|
5540
|
+
managedFiles: [
|
|
5541
|
+
{
|
|
5542
|
+
path: canonicalWorkflowRelativePath,
|
|
5543
|
+
checksum: checksum(workflow),
|
|
5544
|
+
encoding: 'utf8',
|
|
5545
|
+
owner: 'admin-portal-generated',
|
|
5546
|
+
},
|
|
5547
|
+
],
|
|
5548
|
+
generatedFileScope: [canonicalWorkflowRelativePath],
|
|
5549
|
+
};
|
|
5550
|
+
return { workflow: Buffer.from(workflow), manifest, operation };
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
function contextFixture(t, manifest, workflow = canonical, operation) {
|
|
5554
|
+
const directory = mkdtempSync(join(tmpdir(), 'eai-workflow-context-'));
|
|
5555
|
+
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
5556
|
+
mkdirSync(join(directory, '.github/workflows'), { recursive: true });
|
|
5557
|
+
mkdirSync(join(directory, 'tests/fixtures'), { recursive: true });
|
|
5558
|
+
mkdirSync(join(directory, fixtureDirectory), { recursive: true });
|
|
5559
|
+
writeFileSync(join(directory, fixtureDirectory, 'eai-app.yml'), canonical);
|
|
5560
|
+
writeFileSync(
|
|
5561
|
+
join(directory, fixtureDirectory, 'generated-eai-app.yml'),
|
|
5562
|
+
generated,
|
|
5563
|
+
);
|
|
5564
|
+
writeFileSync(join(directory, canonicalWorkflowRelativePath), workflow);
|
|
5565
|
+
if (manifest !== undefined)
|
|
5566
|
+
writeFileSync(
|
|
5567
|
+
join(directory, '.eai-manifest.json'),
|
|
5568
|
+
JSON.stringify(manifest),
|
|
5569
|
+
);
|
|
5570
|
+
if (operation !== undefined) {
|
|
5571
|
+
mkdirSync(join(directory, '.eai'));
|
|
5572
|
+
writeFileSync(
|
|
5573
|
+
join(directory, '.eai/generated-source-operation.json'),
|
|
5574
|
+
JSON.stringify(operation),
|
|
5575
|
+
);
|
|
5576
|
+
}
|
|
5577
|
+
return directory;
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
test('template context requires byte-identical canonical workflow', (t) => {
|
|
5581
|
+
const context = resolveDeploymentWorkflowContext(contextFixture(t));
|
|
5582
|
+
assert.equal(context.context, 'template');
|
|
5583
|
+
});
|
|
5584
|
+
|
|
5585
|
+
test('generated operation and tenant ID grammar matches the collector admission boundary', () => {
|
|
5586
|
+
assert.ok(
|
|
5587
|
+
regularBytes(evidenceScript).toString('utf8').includes(
|
|
5588
|
+
`const SAFE_PATH_SEGMENT = ${runtimeSafePathSegmentPattern.toString()};`,
|
|
5589
|
+
),
|
|
5590
|
+
);
|
|
5591
|
+
});
|
|
5592
|
+
|
|
5593
|
+
for (const field of ['operationId', 'tenantId']) {
|
|
5594
|
+
for (const value of ['a', 'source-123', 'source:operation_1.2', 'f6b8f71d-ef47-afc1-58e0-504f1ae99950', 'a'.repeat(256)])
|
|
5595
|
+
test(`generated ${field} accepts safe collector ID ${JSON.stringify(value)}`, (t) => {
|
|
5596
|
+
const fixture = exportedFixture(
|
|
5597
|
+
field === 'tenantId' ? { TARGET_TENANT_ID: value } : {},
|
|
5598
|
+
);
|
|
5599
|
+
fixture.operation[field] = value;
|
|
5600
|
+
assert.equal(
|
|
5601
|
+
resolveDeploymentWorkflowContext(
|
|
5602
|
+
contextFixture(t, fixture.manifest, fixture.workflow, fixture.operation),
|
|
5603
|
+
).context,
|
|
5604
|
+
'admin-portal-generated',
|
|
5605
|
+
);
|
|
5606
|
+
});
|
|
5607
|
+
|
|
5608
|
+
for (const value of [
|
|
5609
|
+
'',
|
|
5610
|
+
'../other',
|
|
5611
|
+
'source/other',
|
|
5612
|
+
'source?other',
|
|
5613
|
+
'source#other',
|
|
5614
|
+
'source\\other',
|
|
5615
|
+
'source other',
|
|
5616
|
+
'source\nother',
|
|
5617
|
+
'source\n',
|
|
5618
|
+
'source\r\n',
|
|
5619
|
+
'source\0',
|
|
5620
|
+
'-source',
|
|
5621
|
+
'source-',
|
|
5622
|
+
'source.',
|
|
5623
|
+
'source:',
|
|
5624
|
+
'source_',
|
|
5625
|
+
'a'.repeat(257),
|
|
5626
|
+
123,
|
|
5627
|
+
])
|
|
5628
|
+
test(`generated ${field} rejects unsafe collector ID ${JSON.stringify(value)}`, (t) => {
|
|
5629
|
+
const fixture = exportedFixture(
|
|
5630
|
+
field === 'tenantId' ? { TARGET_TENANT_ID: value } : {},
|
|
5631
|
+
);
|
|
5632
|
+
fixture.operation[field] = value;
|
|
5633
|
+
assert.throws(
|
|
5634
|
+
() =>
|
|
5635
|
+
resolveDeploymentWorkflowContext(
|
|
5636
|
+
contextFixture(t, fixture.manifest, fixture.workflow, fixture.operation),
|
|
5637
|
+
),
|
|
5638
|
+
/must match collector safe path grammar/,
|
|
5639
|
+
);
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5642
|
+
|
|
5643
|
+
test('repository workflow is validated against its actual declared context', () => {
|
|
5644
|
+
assert.ok(
|
|
5645
|
+
['template', 'cli', 'admin-portal-generated'].includes(
|
|
5646
|
+
resolveDeploymentWorkflowContext(root, {
|
|
5647
|
+
controlledRoutingInputPath:
|
|
5648
|
+
process.env.EAI_TEMPLATE_CONTROLLED_ROUTING_INPUT,
|
|
5649
|
+
controlledRoutingConfigPaths: selectedControlledRoutingConfigPaths,
|
|
5650
|
+
}).context,
|
|
5651
|
+
),
|
|
5652
|
+
);
|
|
5653
|
+
});
|
|
5654
|
+
|
|
5655
|
+
test('known CLI project manifest retains actual canonical workflow authority', (t) => {
|
|
5656
|
+
const directory = contextFixture(t, {
|
|
5657
|
+
schemaVersion: 1,
|
|
5658
|
+
cli: { version: '3.19.1' },
|
|
5659
|
+
template: {
|
|
5660
|
+
commit: 'a'.repeat(40),
|
|
5661
|
+
repo: 'https://github.com/eai-support/eai-app-template.git',
|
|
5662
|
+
},
|
|
5663
|
+
gofer: {
|
|
5664
|
+
bundle: { commit: 'b'.repeat(40) },
|
|
5665
|
+
managedFiles: {
|
|
5666
|
+
'.specify/script.mjs': { source: 'bundled', sha256: 'c'.repeat(64) },
|
|
5667
|
+
},
|
|
5668
|
+
},
|
|
5669
|
+
});
|
|
5670
|
+
assert.deepEqual(resolveDeploymentWorkflowContext(directory), {
|
|
5671
|
+
context: 'cli',
|
|
5672
|
+
workflowPath: join(directory, canonicalWorkflowRelativePath),
|
|
5673
|
+
});
|
|
5674
|
+
});
|
|
5675
|
+
|
|
5676
|
+
test('NCB validates its actual generated workflow and keeps all canonical collector controls', (t) => {
|
|
5677
|
+
const fixture = exportedFixture();
|
|
5678
|
+
const directory = contextFixture(
|
|
5679
|
+
t,
|
|
5680
|
+
fixture.manifest,
|
|
5681
|
+
fixture.workflow,
|
|
5682
|
+
fixture.operation,
|
|
5683
|
+
);
|
|
5684
|
+
assert.deepEqual(resolveDeploymentWorkflowContext(directory), {
|
|
5685
|
+
context: 'admin-portal-generated',
|
|
5686
|
+
workflowPath: join(directory, 'tests/fixtures/source-unknown/eai-app.yml'),
|
|
5687
|
+
});
|
|
5688
|
+
});
|
|
5689
|
+
|
|
5690
|
+
test('declared generator inputs vary without changing protected workflow code', () => {
|
|
5691
|
+
const fixture = exportedFixture({
|
|
5692
|
+
DEFAULT_BRANCH: 'release/main',
|
|
5693
|
+
APP_KEY: 'main',
|
|
5694
|
+
TARGET_TENANT_ID: 'other-customer',
|
|
5695
|
+
MANAGED_REVIEW_OWNER: 'customer-org',
|
|
5696
|
+
});
|
|
5697
|
+
validateGeneratedWorkflow(
|
|
5698
|
+
fixture.manifest,
|
|
5699
|
+
fixture.workflow,
|
|
5700
|
+
generated,
|
|
5701
|
+
fixture.operation,
|
|
5702
|
+
);
|
|
5703
|
+
});
|
|
5704
|
+
|
|
5705
|
+
for (const [name, mutate] of [
|
|
5706
|
+
[
|
|
5707
|
+
'unsupported schema',
|
|
5708
|
+
(f) => {
|
|
5709
|
+
f.manifest.schemaVersion = 'unknown';
|
|
5710
|
+
},
|
|
5711
|
+
],
|
|
5712
|
+
[
|
|
5713
|
+
'wrong source mode',
|
|
5714
|
+
(f) => {
|
|
5715
|
+
f.manifest.sourceMode = 'eai-cli-generated';
|
|
5716
|
+
},
|
|
5717
|
+
],
|
|
5718
|
+
[
|
|
5719
|
+
'unsafe workflow path',
|
|
5720
|
+
(f) => {
|
|
5721
|
+
f.manifest.workflowPath = '../eai-app.yml';
|
|
5722
|
+
},
|
|
5723
|
+
],
|
|
5724
|
+
[
|
|
5725
|
+
'missing workflow',
|
|
5726
|
+
(f) => {
|
|
5727
|
+
f.manifest.managedFiles = [];
|
|
5728
|
+
f.manifest.generatedFileScope = [];
|
|
5729
|
+
},
|
|
5730
|
+
],
|
|
5731
|
+
[
|
|
5732
|
+
'duplicate scope',
|
|
5733
|
+
(f) => {
|
|
5734
|
+
f.manifest.managedFiles.push({ ...f.manifest.managedFiles[0] });
|
|
5735
|
+
f.manifest.generatedFileScope.push(canonicalWorkflowRelativePath);
|
|
5736
|
+
},
|
|
5737
|
+
],
|
|
5738
|
+
[
|
|
5739
|
+
'checksum mismatch',
|
|
5740
|
+
(f) => {
|
|
5741
|
+
f.manifest.managedFiles[0].checksum = `sha256:${'0'.repeat(64)}`;
|
|
5742
|
+
},
|
|
5743
|
+
],
|
|
5744
|
+
[
|
|
5745
|
+
'scope mismatch',
|
|
5746
|
+
(f) => {
|
|
5747
|
+
f.manifest.generatedFileScope = [];
|
|
5748
|
+
},
|
|
5749
|
+
],
|
|
5750
|
+
[
|
|
5751
|
+
'wrong tenant',
|
|
5752
|
+
(f) => {
|
|
5753
|
+
f.operation.tenantId = 'wrong-customer';
|
|
5754
|
+
},
|
|
5755
|
+
],
|
|
5756
|
+
[
|
|
5757
|
+
'wrong app',
|
|
5758
|
+
(f) => {
|
|
5759
|
+
f.operation.appKey = 'wrong-app';
|
|
5760
|
+
},
|
|
5761
|
+
],
|
|
5762
|
+
[
|
|
5763
|
+
'unknown operation field',
|
|
5764
|
+
(f) => {
|
|
5765
|
+
f.operation.callbackUrl = 'https://attacker.example';
|
|
5766
|
+
},
|
|
5767
|
+
],
|
|
5768
|
+
])
|
|
5769
|
+
test(`rejects ${name} instead of falling back to fixture`, (t) => {
|
|
5770
|
+
const fixture = exportedFixture();
|
|
5771
|
+
mutate(fixture);
|
|
5772
|
+
assert.throws(() =>
|
|
5773
|
+
resolveDeploymentWorkflowContext(
|
|
5774
|
+
contextFixture(
|
|
5775
|
+
t,
|
|
5776
|
+
fixture.manifest,
|
|
5777
|
+
fixture.workflow,
|
|
5778
|
+
fixture.operation,
|
|
5779
|
+
),
|
|
5780
|
+
),
|
|
5781
|
+
);
|
|
5782
|
+
});
|
|
5783
|
+
|
|
5784
|
+
for (const [name, before, after] of [
|
|
5785
|
+
[
|
|
5786
|
+
'OIDC permission widening',
|
|
5787
|
+
' contents: read\n steps:\n - name: Checkout unprivileged',
|
|
5788
|
+
' contents: read\n id-token: write\n steps:\n - name: Checkout unprivileged',
|
|
5789
|
+
],
|
|
5790
|
+
[
|
|
5791
|
+
'validation before image publication',
|
|
5792
|
+
' - name: Typecheck\n id: typecheck',
|
|
5793
|
+
' - name: Typecheck\n if: false\n id: typecheck',
|
|
5794
|
+
],
|
|
5795
|
+
[
|
|
5796
|
+
'same-repository review binding',
|
|
5797
|
+
'github.event.pull_request.head.repo.id == github.event.repository.id',
|
|
5798
|
+
'true',
|
|
5799
|
+
],
|
|
5800
|
+
[
|
|
5801
|
+
'merged source authority',
|
|
5802
|
+
'pr.merge_commit_sha === process.env.GITHUB_SHA',
|
|
5803
|
+
'true',
|
|
5804
|
+
],
|
|
5805
|
+
['nonce required', '!value || /[\\r\\n]/.test(value)', 'false'],
|
|
5806
|
+
[
|
|
5807
|
+
'canonical operation route',
|
|
5808
|
+
'/source-preparations/${encodeURIComponent(binding.operationId)}/workflow-evidence',
|
|
5809
|
+
'/workflow-evidence',
|
|
5810
|
+
],
|
|
5811
|
+
[
|
|
5812
|
+
'current artifact digest',
|
|
5813
|
+
'${{ steps.image-artifact.outputs.artifact-digest }}',
|
|
5814
|
+
'${{ secrets.ARBITRARY_DIGEST }}',
|
|
5815
|
+
],
|
|
5816
|
+
[
|
|
5817
|
+
'validation outcomes checked',
|
|
5818
|
+
"outcomes.some(value => value !== 'success')",
|
|
5819
|
+
'false',
|
|
5820
|
+
],
|
|
5821
|
+
[
|
|
5822
|
+
'no-follow producer read',
|
|
5823
|
+
'fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK',
|
|
5824
|
+
'fs.constants.O_RDONLY',
|
|
5825
|
+
],
|
|
5826
|
+
[
|
|
5827
|
+
'conditional OIDC issuance',
|
|
5828
|
+
" id: github-oidc\n if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && inputs.handover_to_cli)",
|
|
5829
|
+
' id: github-oidc',
|
|
5830
|
+
],
|
|
5831
|
+
[
|
|
5832
|
+
'trusted callback',
|
|
5833
|
+
'https://dev-admin-portal.myenterprise.ai/api/platform/generated-apps/managed-review/workflow',
|
|
5834
|
+
'http://attacker.example/callback',
|
|
5835
|
+
],
|
|
5836
|
+
])
|
|
5837
|
+
test(`rejects workflow tamper: ${name}, even with recomputed managed checksum`, () => {
|
|
5838
|
+
const fixture = exportedFixture();
|
|
5839
|
+
assert.ok(
|
|
5840
|
+
fixture.workflow.toString().includes(before),
|
|
5841
|
+
'Negative must alter actual contract bytes.',
|
|
5842
|
+
);
|
|
5843
|
+
fixture.workflow = Buffer.from(
|
|
5844
|
+
fixture.workflow.toString().replace(before, after),
|
|
5845
|
+
);
|
|
5846
|
+
fixture.manifest.managedFiles[0].checksum = checksum(fixture.workflow);
|
|
5847
|
+
assert.throws(() =>
|
|
5848
|
+
validateGeneratedWorkflow(
|
|
5849
|
+
fixture.manifest,
|
|
5850
|
+
fixture.workflow,
|
|
5851
|
+
generated,
|
|
5852
|
+
fixture.operation,
|
|
5853
|
+
),
|
|
5854
|
+
);
|
|
5855
|
+
});
|
|
5856
|
+
|
|
5857
|
+
test('unknown or malformed CLI context cannot suppress actual workflow checks', (t) => {
|
|
5858
|
+
for (const manifest of [
|
|
5859
|
+
{ schemaVersion: 2 },
|
|
5860
|
+
{ schemaVersion: 1, sourceMode: 'admin-portal-generated' },
|
|
5861
|
+
{
|
|
5862
|
+
schemaVersion: 1,
|
|
5863
|
+
gofer: {
|
|
5864
|
+
managedFiles: {
|
|
5865
|
+
'../escape': { sha256: 'a'.repeat(64), source: 'bundled' },
|
|
5866
|
+
},
|
|
5867
|
+
},
|
|
5868
|
+
},
|
|
5869
|
+
]) {
|
|
5870
|
+
assert.throws(() =>
|
|
5871
|
+
resolveDeploymentWorkflowContext(contextFixture(t, manifest)),
|
|
5872
|
+
);
|
|
5873
|
+
}
|
|
5874
|
+
});
|
|
5875
|
+
|
|
5876
|
+
test('fixture drift and template workflow drift fail closed', (t) => {
|
|
5877
|
+
const directory = contextFixture(t);
|
|
5878
|
+
writeFileSync(
|
|
5879
|
+
join(directory, canonicalWorkflowRelativePath),
|
|
5880
|
+
'name: changed\n',
|
|
5881
|
+
);
|
|
5882
|
+
assert.throws(() => resolveDeploymentWorkflowContext(directory));
|
|
5883
|
+
const fixture = exportedFixture();
|
|
5884
|
+
const generatedDirectory = contextFixture(
|
|
5885
|
+
t,
|
|
5886
|
+
fixture.manifest,
|
|
5887
|
+
fixture.workflow,
|
|
5888
|
+
fixture.operation,
|
|
5889
|
+
);
|
|
5890
|
+
writeFileSync(
|
|
5891
|
+
join(generatedDirectory, 'tests/fixtures/source-unknown/eai-app.yml'),
|
|
5892
|
+
'name: changed\n',
|
|
5893
|
+
);
|
|
5894
|
+
assert.throws(() => resolveDeploymentWorkflowContext(generatedDirectory));
|
|
5895
|
+
});
|
|
5896
|
+
|
|
5897
|
+
test('generated contract fixture drift fails before comparison', () => {
|
|
5898
|
+
const fixture = exportedFixture();
|
|
5899
|
+
assert.throws(() =>
|
|
5900
|
+
validateGeneratedWorkflow(
|
|
5901
|
+
fixture.manifest,
|
|
5902
|
+
fixture.workflow,
|
|
5903
|
+
Buffer.from('changed'),
|
|
5904
|
+
fixture.operation,
|
|
5905
|
+
),
|
|
5906
|
+
);
|
|
5907
|
+
});
|
|
5908
|
+
|
|
5909
|
+
test('inline synthetic generated input retains repository and actual normalized export parity', () => {
|
|
5910
|
+
assert.equal(digest(generated), generatedDigest);
|
|
5911
|
+
if (existsSync(fixtures)) {
|
|
5912
|
+
assert.ok(regularBytes(join(fixtures, 'eai-app.yml')).equals(canonical));
|
|
5913
|
+
assert.ok(
|
|
5914
|
+
regularBytes(join(fixtures, 'generated-eai-app.yml')).equals(generated),
|
|
5915
|
+
);
|
|
5916
|
+
} else {
|
|
5917
|
+
assert.ok(
|
|
5918
|
+
['template', 'cli'].includes(
|
|
5919
|
+
resolveDeploymentWorkflowContext(root, {
|
|
5920
|
+
controlledRoutingInputPath:
|
|
5921
|
+
process.env.EAI_TEMPLATE_CONTROLLED_ROUTING_INPUT,
|
|
5922
|
+
controlledRoutingConfigPaths: selectedControlledRoutingConfigPaths,
|
|
5923
|
+
}).context,
|
|
5924
|
+
),
|
|
5925
|
+
);
|
|
5926
|
+
assert.equal(
|
|
5927
|
+
digest(regularBytes(join(root, canonicalWorkflowRelativePath))),
|
|
5928
|
+
canonicalDigest,
|
|
5929
|
+
);
|
|
5930
|
+
}
|
|
5931
|
+
});
|
|
5932
|
+
|
|
5933
|
+
test('legacy four-file evidence repair admits no fixtures and rejects canonical tampering', (t) => {
|
|
5934
|
+
const manifest = {
|
|
5935
|
+
schemaVersion: 1,
|
|
5936
|
+
cli: { version: '3.19.1' },
|
|
5937
|
+
template: {
|
|
5938
|
+
commit: 'a'.repeat(40),
|
|
5939
|
+
repo: 'https://github.com/eai-support/eai-app-template.git',
|
|
5940
|
+
},
|
|
5941
|
+
};
|
|
5942
|
+
const directory = contextFixture(t, manifest);
|
|
5943
|
+
rmSync(join(directory, fixtureDirectory), { recursive: true });
|
|
5944
|
+
assert.deepEqual(resolveDeploymentWorkflowContext(directory), {
|
|
5945
|
+
context: 'cli',
|
|
5946
|
+
workflowPath: join(directory, canonicalWorkflowRelativePath),
|
|
5947
|
+
});
|
|
5948
|
+
writeFileSync(
|
|
5949
|
+
join(directory, canonicalWorkflowRelativePath),
|
|
5950
|
+
Buffer.concat([canonical, Buffer.from('\n# changed\n')]),
|
|
5951
|
+
);
|
|
5952
|
+
assert.throws(
|
|
5953
|
+
() => resolveDeploymentWorkflowContext(directory),
|
|
5954
|
+
/canonical digest/,
|
|
5955
|
+
);
|
|
5956
|
+
});
|
|
5957
|
+
|
|
5958
|
+
function controlledTunnelFixture(
|
|
5959
|
+
t,
|
|
5960
|
+
{
|
|
5961
|
+
evidenceBaseUrl = 'https://ownerrun-8000.aue01.devtunnels.ms',
|
|
5962
|
+
callbackBaseUrl = 'https://ownerrun-3010.aue.devtunnels.ms',
|
|
5963
|
+
} = {},
|
|
5964
|
+
) {
|
|
5965
|
+
const fixture = exportedFixture({
|
|
5966
|
+
EVIDENCE_BASE_URL: evidenceBaseUrl,
|
|
5967
|
+
MANAGED_REVIEW_CALLBACK_URL: callbackBaseUrl + managedReviewPath,
|
|
5968
|
+
});
|
|
5969
|
+
const candidateRoot = contextFixture(
|
|
5970
|
+
t,
|
|
5971
|
+
fixture.manifest,
|
|
5972
|
+
fixture.workflow,
|
|
5973
|
+
fixture.operation,
|
|
5974
|
+
);
|
|
5975
|
+
const externalDirectory = mkdtempSync(
|
|
5976
|
+
join(tmpdir(), 'eai-controlled-routing-'),
|
|
5977
|
+
);
|
|
5978
|
+
t.after(() => rmSync(externalDirectory, { recursive: true, force: true }));
|
|
5979
|
+
const inputPath = join(realpathSync(externalDirectory), 'owner-routing.json');
|
|
5980
|
+
const configPaths = {
|
|
5981
|
+
publicApi: join(realpathSync(externalDirectory), 'public-api.json'),
|
|
5982
|
+
portal: join(realpathSync(externalDirectory), 'portal.json'),
|
|
5983
|
+
};
|
|
5984
|
+
const safeConfig = {
|
|
5985
|
+
ADMIN_PORTAL_PUBLIC_API_BASE_URL: evidenceBaseUrl,
|
|
5986
|
+
ADMIN_PORTAL_WORKFLOW_CALLBACK_BASE_URL: callbackBaseUrl,
|
|
5987
|
+
};
|
|
5988
|
+
for (const path of Object.values(configPaths))
|
|
5989
|
+
writeFileSync(path, JSON.stringify(safeConfig, null, 2) + '\n', {
|
|
5990
|
+
mode: 0o600,
|
|
5991
|
+
});
|
|
5992
|
+
const proof = {
|
|
5993
|
+
schemaVersion: 'eai.template_controlled_routing.v1',
|
|
5994
|
+
mode: 'owner-controlled-qualification-only',
|
|
5995
|
+
appKey: fixture.manifest.appKey,
|
|
5996
|
+
tenantId: fixture.operation.tenantId,
|
|
5997
|
+
operationId: fixture.operation.operationId,
|
|
5998
|
+
workflowSha256: digest(fixture.workflow),
|
|
5999
|
+
generatedOperationSha256: digest(
|
|
6000
|
+
regularBytes(join(candidateRoot, '.eai/generated-source-operation.json')),
|
|
6001
|
+
),
|
|
6002
|
+
evidenceBaseUrl,
|
|
6003
|
+
managedReviewCallbackUrl: callbackBaseUrl + managedReviewPath,
|
|
6004
|
+
sourceConfigSha256: {
|
|
6005
|
+
publicApi: digest(regularBytes(configPaths.publicApi)),
|
|
6006
|
+
portal: digest(regularBytes(configPaths.portal)),
|
|
6007
|
+
},
|
|
6008
|
+
};
|
|
6009
|
+
writeFileSync(inputPath, JSON.stringify(proof, null, 2) + '\n', {
|
|
6010
|
+
mode: 0o600,
|
|
6011
|
+
});
|
|
6012
|
+
return {
|
|
6013
|
+
fixture,
|
|
6014
|
+
candidateRoot,
|
|
6015
|
+
externalDirectory,
|
|
6016
|
+
inputPath,
|
|
6017
|
+
proof,
|
|
6018
|
+
configPaths,
|
|
6019
|
+
};
|
|
6020
|
+
}
|
|
6021
|
+
|
|
6022
|
+
function supportsControlledRouting() {
|
|
6023
|
+
return process.platform !== 'win32' && typeof process.getuid === 'function';
|
|
6024
|
+
}
|
|
6025
|
+
|
|
6026
|
+
function assertControlledRoutingUnsupported(inputPath, candidateRoot) {
|
|
6027
|
+
assert.throws(
|
|
6028
|
+
() => readControlledRoutingProof(inputPath, candidateRoot),
|
|
6029
|
+
/POSIX owner protection/,
|
|
6030
|
+
);
|
|
6031
|
+
}
|
|
6032
|
+
|
|
6033
|
+
test('canonical app keys admit the exact generated-runtime boundaries', () => {
|
|
6034
|
+
for (const appKey of ['ab', 'a'.repeat(63), 'a--']) {
|
|
6035
|
+
const fixture = exportedFixture({ APP_KEY: appKey });
|
|
6036
|
+
validateGeneratedWorkflow(
|
|
6037
|
+
fixture.manifest,
|
|
6038
|
+
fixture.workflow,
|
|
6039
|
+
generated,
|
|
6040
|
+
fixture.operation,
|
|
6041
|
+
);
|
|
6042
|
+
}
|
|
6043
|
+
});
|
|
6044
|
+
|
|
6045
|
+
for (const appKey of ['a', '1app', 'a'.repeat(64), 'Aapp', 'bad_key', '']) {
|
|
6046
|
+
test(
|
|
6047
|
+
'rejects generated-runtime-invalid app key ' + JSON.stringify(appKey),
|
|
6048
|
+
() => {
|
|
6049
|
+
const fixture = exportedFixture({ APP_KEY: appKey });
|
|
6050
|
+
assert.throws(() =>
|
|
6051
|
+
validateGeneratedWorkflow(
|
|
6052
|
+
fixture.manifest,
|
|
6053
|
+
fixture.workflow,
|
|
6054
|
+
generated,
|
|
6055
|
+
fixture.operation,
|
|
6056
|
+
),
|
|
6057
|
+
);
|
|
6058
|
+
},
|
|
6059
|
+
);
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
test('source operation independently rejects a noncanonical app key', () => {
|
|
6063
|
+
const fixture = exportedFixture();
|
|
6064
|
+
fixture.operation.appKey = '1app';
|
|
6065
|
+
assert.throws(() =>
|
|
6066
|
+
validateGeneratedWorkflow(
|
|
6067
|
+
fixture.manifest,
|
|
6068
|
+
fixture.workflow,
|
|
6069
|
+
generated,
|
|
6070
|
+
fixture.operation,
|
|
6071
|
+
),
|
|
6072
|
+
);
|
|
6073
|
+
});
|
|
6074
|
+
|
|
6075
|
+
test('every trusted deployed Portal cell pair is explicit and exact', () => {
|
|
6076
|
+
for (const [evidence, callback] of trustedRoutingPairs) {
|
|
6077
|
+
for (const ending of ['', '/']) {
|
|
6078
|
+
const fixture = exportedFixture({
|
|
6079
|
+
EVIDENCE_BASE_URL: evidence + ending,
|
|
6080
|
+
MANAGED_REVIEW_CALLBACK_URL: callback,
|
|
6081
|
+
});
|
|
6082
|
+
validateGeneratedWorkflow(
|
|
6083
|
+
fixture.manifest,
|
|
6084
|
+
fixture.workflow,
|
|
6085
|
+
generated,
|
|
6086
|
+
fixture.operation,
|
|
6087
|
+
);
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
});
|
|
6091
|
+
|
|
6092
|
+
for (const [name, inputs] of [
|
|
6093
|
+
[
|
|
6094
|
+
'hostile evidence host',
|
|
6095
|
+
{ EVIDENCE_BASE_URL: 'https://attacker.example/public' },
|
|
6096
|
+
],
|
|
6097
|
+
[
|
|
6098
|
+
'suffix evidence host',
|
|
6099
|
+
{
|
|
6100
|
+
EVIDENCE_BASE_URL:
|
|
6101
|
+
'https://dev-api.au.myenterprise.ai.attacker.example/public',
|
|
6102
|
+
},
|
|
6103
|
+
],
|
|
6104
|
+
[
|
|
6105
|
+
'subdomain evidence host',
|
|
6106
|
+
{ EVIDENCE_BASE_URL: 'https://evil.dev-api.au.myenterprise.ai/public' },
|
|
6107
|
+
],
|
|
6108
|
+
[
|
|
6109
|
+
'evidence credentials',
|
|
6110
|
+
{
|
|
6111
|
+
EVIDENCE_BASE_URL:
|
|
6112
|
+
'https://user:password@dev-api.au.myenterprise.ai/public',
|
|
6113
|
+
},
|
|
6114
|
+
],
|
|
6115
|
+
[
|
|
6116
|
+
'evidence explicit default port',
|
|
6117
|
+
{ EVIDENCE_BASE_URL: 'https://dev-api.au.myenterprise.ai:443/public' },
|
|
6118
|
+
],
|
|
6119
|
+
[
|
|
6120
|
+
'evidence alternate port',
|
|
6121
|
+
{ EVIDENCE_BASE_URL: 'https://dev-api.au.myenterprise.ai:8443/public' },
|
|
6122
|
+
],
|
|
6123
|
+
[
|
|
6124
|
+
'evidence wrong path',
|
|
6125
|
+
{ EVIDENCE_BASE_URL: 'https://dev-api.au.myenterprise.ai/attacker' },
|
|
6126
|
+
],
|
|
6127
|
+
[
|
|
6128
|
+
'evidence query',
|
|
6129
|
+
{
|
|
6130
|
+
EVIDENCE_BASE_URL:
|
|
6131
|
+
'https://dev-api.au.myenterprise.ai/public?redirect=attacker',
|
|
6132
|
+
},
|
|
6133
|
+
],
|
|
6134
|
+
[
|
|
6135
|
+
'evidence fragment',
|
|
6136
|
+
{ EVIDENCE_BASE_URL: 'https://dev-api.au.myenterprise.ai/public#attacker' },
|
|
6137
|
+
],
|
|
6138
|
+
[
|
|
6139
|
+
'hostile callback host',
|
|
6140
|
+
{
|
|
6141
|
+
MANAGED_REVIEW_CALLBACK_URL:
|
|
6142
|
+
'https://attacker.example' + managedReviewPath,
|
|
6143
|
+
},
|
|
6144
|
+
],
|
|
6145
|
+
[
|
|
6146
|
+
'callback path suffix',
|
|
6147
|
+
{
|
|
6148
|
+
MANAGED_REVIEW_CALLBACK_URL:
|
|
6149
|
+
'https://dev-admin-portal.myenterprise.ai/evil' + managedReviewPath,
|
|
6150
|
+
},
|
|
6151
|
+
],
|
|
6152
|
+
[
|
|
6153
|
+
'callback query',
|
|
6154
|
+
{
|
|
6155
|
+
MANAGED_REVIEW_CALLBACK_URL:
|
|
6156
|
+
'https://dev-admin-portal.myenterprise.ai' +
|
|
6157
|
+
managedReviewPath +
|
|
6158
|
+
'?redirect=attacker',
|
|
6159
|
+
},
|
|
6160
|
+
],
|
|
6161
|
+
[
|
|
6162
|
+
'callback explicit port',
|
|
6163
|
+
{
|
|
6164
|
+
MANAGED_REVIEW_CALLBACK_URL:
|
|
6165
|
+
'https://dev-admin-portal.myenterprise.ai:443' + managedReviewPath,
|
|
6166
|
+
},
|
|
6167
|
+
],
|
|
6168
|
+
[
|
|
6169
|
+
'cross-environment evidence',
|
|
6170
|
+
{ EVIDENCE_BASE_URL: 'https://api.au.myenterprise.ai/public' },
|
|
6171
|
+
],
|
|
6172
|
+
[
|
|
6173
|
+
'cross-region callback',
|
|
6174
|
+
{
|
|
6175
|
+
EVIDENCE_BASE_URL: 'https://api.au.myenterprise.ai/public',
|
|
6176
|
+
MANAGED_REVIEW_CALLBACK_URL:
|
|
6177
|
+
'https://admin-portal.eu.myenterprise.ai' + managedReviewPath,
|
|
6178
|
+
},
|
|
6179
|
+
],
|
|
6180
|
+
]) {
|
|
6181
|
+
test('rejects recomputed-manifest workflow with ' + name, () => {
|
|
6182
|
+
const fixture = exportedFixture(inputs);
|
|
6183
|
+
assert.throws(() =>
|
|
6184
|
+
validateGeneratedWorkflow(
|
|
6185
|
+
fixture.manifest,
|
|
6186
|
+
fixture.workflow,
|
|
6187
|
+
generated,
|
|
6188
|
+
fixture.operation,
|
|
6189
|
+
),
|
|
6190
|
+
);
|
|
6191
|
+
});
|
|
6192
|
+
}
|
|
6193
|
+
|
|
6194
|
+
test('local tunnel destinations never fall back to repository-derived trust', (t) => {
|
|
6195
|
+
const f = controlledTunnelFixture(t);
|
|
6196
|
+
assert.throws(
|
|
6197
|
+
() => resolveDeploymentWorkflowContext(f.candidateRoot),
|
|
6198
|
+
/exact trusted Portal cell pair/,
|
|
6199
|
+
);
|
|
6200
|
+
});
|
|
6201
|
+
|
|
6202
|
+
test('explicit owner-controlled tunnel qualification binds raw operation and workflow without claiming live authority', (t) => {
|
|
6203
|
+
const f = controlledTunnelFixture(t);
|
|
6204
|
+
if (!supportsControlledRouting()) {
|
|
6205
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6206
|
+
return;
|
|
6207
|
+
}
|
|
6208
|
+
assert.equal(
|
|
6209
|
+
resolveDeploymentWorkflowContext(f.candidateRoot, {
|
|
6210
|
+
controlledRoutingInputPath: f.inputPath,
|
|
6211
|
+
controlledRoutingConfigPaths: f.configPaths,
|
|
6212
|
+
}).context,
|
|
6213
|
+
'admin-portal-generated',
|
|
6214
|
+
);
|
|
6215
|
+
});
|
|
6216
|
+
|
|
6217
|
+
for (const [name, mutate] of [
|
|
6218
|
+
[
|
|
6219
|
+
'wrong app',
|
|
6220
|
+
(p) => {
|
|
6221
|
+
p.appKey = 'different-app';
|
|
6222
|
+
},
|
|
6223
|
+
],
|
|
6224
|
+
[
|
|
6225
|
+
'wrong tenant',
|
|
6226
|
+
(p) => {
|
|
6227
|
+
p.tenantId = 'different-tenant';
|
|
6228
|
+
},
|
|
6229
|
+
],
|
|
6230
|
+
[
|
|
6231
|
+
'wrong operation',
|
|
6232
|
+
(p) => {
|
|
6233
|
+
p.operationId = 'source-different';
|
|
6234
|
+
},
|
|
6235
|
+
],
|
|
6236
|
+
[
|
|
6237
|
+
'workflow bytes drift',
|
|
6238
|
+
(p) => {
|
|
6239
|
+
p.workflowSha256 = '0'.repeat(64);
|
|
6240
|
+
},
|
|
6241
|
+
],
|
|
6242
|
+
[
|
|
6243
|
+
'operation bytes drift',
|
|
6244
|
+
(p) => {
|
|
6245
|
+
p.generatedOperationSha256 = '0'.repeat(64);
|
|
6246
|
+
},
|
|
6247
|
+
],
|
|
6248
|
+
[
|
|
6249
|
+
'extra input fields',
|
|
6250
|
+
(p) => {
|
|
6251
|
+
p.actorAuthorization = true;
|
|
6252
|
+
},
|
|
6253
|
+
],
|
|
6254
|
+
[
|
|
6255
|
+
'noncontrolled mode',
|
|
6256
|
+
(p) => {
|
|
6257
|
+
p.mode = 'live-authorized';
|
|
6258
|
+
},
|
|
6259
|
+
],
|
|
6260
|
+
[
|
|
6261
|
+
'missing source config',
|
|
6262
|
+
(p) => {
|
|
6263
|
+
delete p.sourceConfigSha256;
|
|
6264
|
+
},
|
|
6265
|
+
],
|
|
6266
|
+
[
|
|
6267
|
+
'malformed source digest',
|
|
6268
|
+
(p) => {
|
|
6269
|
+
p.sourceConfigSha256.portal = '';
|
|
6270
|
+
},
|
|
6271
|
+
],
|
|
6272
|
+
[
|
|
6273
|
+
'hostile local authority',
|
|
6274
|
+
(p) => {
|
|
6275
|
+
p.evidenceBaseUrl = 'https://attacker.example/public';
|
|
6276
|
+
},
|
|
6277
|
+
],
|
|
6278
|
+
[
|
|
6279
|
+
'wrong tunnel port',
|
|
6280
|
+
(p) => {
|
|
6281
|
+
p.evidenceBaseUrl = 'https://ownerrun-8001.aue.devtunnels.ms';
|
|
6282
|
+
},
|
|
6283
|
+
],
|
|
6284
|
+
[
|
|
6285
|
+
'wrong callback tunnel port',
|
|
6286
|
+
(p) => {
|
|
6287
|
+
p.managedReviewCallbackUrl =
|
|
6288
|
+
'https://ownerrun-3000.aue.devtunnels.ms' + managedReviewPath;
|
|
6289
|
+
},
|
|
6290
|
+
],
|
|
6291
|
+
[
|
|
6292
|
+
'different local evidence URL',
|
|
6293
|
+
(p) => {
|
|
6294
|
+
p.evidenceBaseUrl = 'https://otherowner-8000.aue.devtunnels.ms';
|
|
6295
|
+
},
|
|
6296
|
+
],
|
|
6297
|
+
]) {
|
|
6298
|
+
test('controlled tunnel input rejects ' + name, (t) => {
|
|
6299
|
+
const f = controlledTunnelFixture(t);
|
|
6300
|
+
mutate(f.proof);
|
|
6301
|
+
writeFileSync(f.inputPath, JSON.stringify(f.proof, null, 2) + '\n');
|
|
6302
|
+
if (!supportsControlledRouting()) {
|
|
6303
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6304
|
+
return;
|
|
6305
|
+
}
|
|
6306
|
+
assert.throws(() =>
|
|
6307
|
+
resolveDeploymentWorkflowContext(f.candidateRoot, {
|
|
6308
|
+
controlledRoutingInputPath: f.inputPath,
|
|
6309
|
+
controlledRoutingConfigPaths: f.configPaths,
|
|
6310
|
+
}),
|
|
6311
|
+
);
|
|
6312
|
+
});
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6315
|
+
for (const [name, alter] of [
|
|
6316
|
+
[
|
|
6317
|
+
'missing input',
|
|
6318
|
+
(f) => {
|
|
6319
|
+
rmSync(f.inputPath);
|
|
6320
|
+
},
|
|
6321
|
+
],
|
|
6322
|
+
[
|
|
6323
|
+
'candidate-repository input',
|
|
6324
|
+
(f) => {
|
|
6325
|
+
const inside = join(realpathSync(f.candidateRoot), 'owner-routing.json');
|
|
6326
|
+
writeFileSync(inside, JSON.stringify(f.proof, null, 2) + '\n', {
|
|
6327
|
+
mode: 0o600,
|
|
6328
|
+
});
|
|
6329
|
+
f.inputPath = inside;
|
|
6330
|
+
},
|
|
6331
|
+
],
|
|
6332
|
+
[
|
|
6333
|
+
'symlink input',
|
|
6334
|
+
(f) => {
|
|
6335
|
+
const link = join(realpathSync(f.externalDirectory), 'symlink.json');
|
|
6336
|
+
symlinkSync(f.inputPath, link);
|
|
6337
|
+
f.inputPath = link;
|
|
6338
|
+
},
|
|
6339
|
+
],
|
|
6340
|
+
[
|
|
6341
|
+
'hardlink input',
|
|
6342
|
+
(f) => {
|
|
6343
|
+
linkSync(
|
|
6344
|
+
f.inputPath,
|
|
6345
|
+
join(realpathSync(f.externalDirectory), 'hardlink.json'),
|
|
6346
|
+
);
|
|
6347
|
+
},
|
|
6348
|
+
],
|
|
6349
|
+
[
|
|
6350
|
+
'unsafe file permissions',
|
|
6351
|
+
(f) => {
|
|
6352
|
+
chmodSync(f.inputPath, 0o644);
|
|
6353
|
+
},
|
|
6354
|
+
],
|
|
6355
|
+
[
|
|
6356
|
+
'unsafe parent permissions',
|
|
6357
|
+
(f) => {
|
|
6358
|
+
chmodSync(f.externalDirectory, 0o755);
|
|
6359
|
+
},
|
|
6360
|
+
],
|
|
6361
|
+
[
|
|
6362
|
+
'oversized input',
|
|
6363
|
+
(f) => {
|
|
6364
|
+
writeFileSync(f.inputPath, ' '.repeat(8193));
|
|
6365
|
+
},
|
|
6366
|
+
],
|
|
6367
|
+
[
|
|
6368
|
+
'malformed input',
|
|
6369
|
+
(f) => {
|
|
6370
|
+
writeFileSync(f.inputPath, '{');
|
|
6371
|
+
},
|
|
6372
|
+
],
|
|
6373
|
+
[
|
|
6374
|
+
'duplicate JSON fields',
|
|
6375
|
+
(f) => {
|
|
6376
|
+
writeFileSync(
|
|
6377
|
+
f.inputPath,
|
|
6378
|
+
'{"mode":"owner-controlled-qualification-only",' +
|
|
6379
|
+
JSON.stringify(f.proof).slice(1),
|
|
6380
|
+
);
|
|
6381
|
+
},
|
|
6382
|
+
],
|
|
6383
|
+
]) {
|
|
6384
|
+
test('controlled tunnel input rejects ' + name, (t) => {
|
|
6385
|
+
const f = controlledTunnelFixture(t);
|
|
6386
|
+
if (!supportsControlledRouting()) {
|
|
6387
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6388
|
+
return;
|
|
6389
|
+
}
|
|
6390
|
+
alter(f);
|
|
6391
|
+
assert.throws(() =>
|
|
6392
|
+
readControlledRoutingProof(f.inputPath, f.candidateRoot, f.configPaths),
|
|
6393
|
+
);
|
|
6394
|
+
});
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
test('controlled proof rejects changed input bytes after admission', (t) => {
|
|
6398
|
+
const f = controlledTunnelFixture(t);
|
|
6399
|
+
if (!supportsControlledRouting()) {
|
|
6400
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6401
|
+
return;
|
|
6402
|
+
}
|
|
6403
|
+
const handle = readControlledRoutingProof(
|
|
6404
|
+
f.inputPath,
|
|
6405
|
+
f.candidateRoot,
|
|
6406
|
+
f.configPaths,
|
|
6407
|
+
);
|
|
6408
|
+
const operationBytes = regularBytes(
|
|
6409
|
+
join(f.candidateRoot, '.eai/generated-source-operation.json'),
|
|
6410
|
+
);
|
|
6411
|
+
f.proof.operationId = 'source-changed';
|
|
6412
|
+
writeFileSync(f.inputPath, JSON.stringify(f.proof, null, 2) + '\n');
|
|
6413
|
+
assert.throws(
|
|
6414
|
+
() =>
|
|
6415
|
+
validateGeneratedWorkflow(
|
|
6416
|
+
f.fixture.manifest,
|
|
6417
|
+
f.fixture.workflow,
|
|
6418
|
+
generated,
|
|
6419
|
+
f.fixture.operation,
|
|
6420
|
+
handle,
|
|
6421
|
+
operationBytes,
|
|
6422
|
+
),
|
|
6423
|
+
/changed after capture/,
|
|
6424
|
+
);
|
|
6425
|
+
});
|
|
6426
|
+
|
|
6427
|
+
test('caller cannot fabricate an opaque controlled routing admission', () => {
|
|
6428
|
+
const f = exportedFixture();
|
|
6429
|
+
assert.throws(
|
|
6430
|
+
() =>
|
|
6431
|
+
validateGeneratedWorkflow(
|
|
6432
|
+
f.manifest,
|
|
6433
|
+
f.workflow,
|
|
6434
|
+
generated,
|
|
6435
|
+
f.operation,
|
|
6436
|
+
{},
|
|
6437
|
+
),
|
|
6438
|
+
/Unrecognized controlled routing proof/,
|
|
6439
|
+
);
|
|
6440
|
+
});
|
|
6441
|
+
|
|
6442
|
+
for (const permissions of ['input', 'parent']) {
|
|
6443
|
+
test(
|
|
6444
|
+
'controlled proof rejects changed ' +
|
|
6445
|
+
permissions +
|
|
6446
|
+
' permissions after admission',
|
|
6447
|
+
(t) => {
|
|
6448
|
+
const f = controlledTunnelFixture(t);
|
|
6449
|
+
if (!supportsControlledRouting()) {
|
|
6450
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6451
|
+
return;
|
|
6452
|
+
}
|
|
6453
|
+
const handle = readControlledRoutingProof(
|
|
6454
|
+
f.inputPath,
|
|
6455
|
+
f.candidateRoot,
|
|
6456
|
+
f.configPaths,
|
|
6457
|
+
);
|
|
6458
|
+
const operationBytes = regularBytes(
|
|
6459
|
+
join(f.candidateRoot, '.eai/generated-source-operation.json'),
|
|
6460
|
+
);
|
|
6461
|
+
chmodSync(
|
|
6462
|
+
permissions === 'input' ? f.inputPath : f.externalDirectory,
|
|
6463
|
+
permissions === 'input' ? 0o644 : 0o755,
|
|
6464
|
+
);
|
|
6465
|
+
assert.throws(
|
|
6466
|
+
() =>
|
|
6467
|
+
validateGeneratedWorkflow(
|
|
6468
|
+
f.fixture.manifest,
|
|
6469
|
+
f.fixture.workflow,
|
|
6470
|
+
generated,
|
|
6471
|
+
f.fixture.operation,
|
|
6472
|
+
handle,
|
|
6473
|
+
operationBytes,
|
|
6474
|
+
),
|
|
6475
|
+
/owner/,
|
|
6476
|
+
);
|
|
6477
|
+
},
|
|
6478
|
+
);
|
|
6479
|
+
}
|
|
6480
|
+
|
|
6481
|
+
test('controlled input rejects a different effective filesystem owner', (t) => {
|
|
6482
|
+
const f = controlledTunnelFixture(t);
|
|
6483
|
+
if (!supportsControlledRouting()) {
|
|
6484
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6485
|
+
return;
|
|
6486
|
+
}
|
|
6487
|
+
const uid = process.getuid();
|
|
6488
|
+
t.mock.method(process, 'getuid', () => uid + 1);
|
|
6489
|
+
assert.throws(
|
|
6490
|
+
() =>
|
|
6491
|
+
readControlledRoutingProof(f.inputPath, f.candidateRoot, f.configPaths),
|
|
6492
|
+
/owner-protected/,
|
|
6493
|
+
);
|
|
6494
|
+
});
|
|
6495
|
+
|
|
6496
|
+
test('non-regular owner input fails without blocking on a FIFO', (t) => {
|
|
6497
|
+
const f = controlledTunnelFixture(t);
|
|
6498
|
+
if (!supportsControlledRouting()) {
|
|
6499
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6500
|
+
return;
|
|
6501
|
+
}
|
|
6502
|
+
rmSync(f.inputPath);
|
|
6503
|
+
execFileSync('mkfifo', [f.inputPath]);
|
|
6504
|
+
chmodSync(f.inputPath, 0o600);
|
|
6505
|
+
const program = `import assert from 'node:assert/strict';
|
|
6506
|
+
import {closeSync,constants as fsConstants,fstatSync,lstatSync,openSync,readSync,realpathSync} from 'node:fs';
|
|
6507
|
+
import {dirname,isAbsolute,relative,resolve} from 'node:path';
|
|
6508
|
+
${controlledReadFlags.toString()}
|
|
6509
|
+
${snapshotControlledDirectories.toString()}
|
|
6510
|
+
${assertControlledDirectories.toString()}
|
|
6511
|
+
${controlledFileMatches.toString()}
|
|
6512
|
+
${readProtectedRoutingBytes.toString()}
|
|
6513
|
+
assert.throws(() => readProtectedRoutingBytes(${JSON.stringify(f.inputPath)}, ${JSON.stringify(f.candidateRoot)}), /bounded owner-only single-link file/);`;
|
|
6514
|
+
const result = spawnSync(
|
|
6515
|
+
process.execPath,
|
|
6516
|
+
['--input-type=module', '-e', program],
|
|
6517
|
+
{ timeout: 2000, encoding: 'utf8' },
|
|
6518
|
+
);
|
|
6519
|
+
assert.equal(
|
|
6520
|
+
result.error,
|
|
6521
|
+
undefined,
|
|
6522
|
+
'FIFO rejection must complete within its bounded child deadline.',
|
|
6523
|
+
);
|
|
6524
|
+
assert.equal(result.signal, null);
|
|
6525
|
+
assert.equal(result.status, 0, result.stderr);
|
|
6526
|
+
});
|
|
6527
|
+
|
|
6528
|
+
for (const flag of ['O_NOFOLLOW', 'O_NONBLOCK']) {
|
|
6529
|
+
test('protected reads reject missing or invalid ' + flag + ' support', () => {
|
|
6530
|
+
for (const value of [undefined, 0, -1, 1.5])
|
|
6531
|
+
assert.throws(
|
|
6532
|
+
() => controlledReadFlags({ ...fsConstants, [flag]: value }),
|
|
6533
|
+
/require.*support/,
|
|
6534
|
+
);
|
|
6535
|
+
});
|
|
6536
|
+
}
|
|
6537
|
+
|
|
6538
|
+
for (const name of ['publicApi', 'portal']) {
|
|
6539
|
+
test(
|
|
6540
|
+
'independent ' + name + ' config rejects invented receipt digests',
|
|
6541
|
+
(t) => {
|
|
6542
|
+
const f = controlledTunnelFixture(t);
|
|
6543
|
+
if (!supportsControlledRouting()) {
|
|
6544
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6545
|
+
return;
|
|
6546
|
+
}
|
|
6547
|
+
f.proof.sourceConfigSha256[name] = '0'.repeat(64);
|
|
6548
|
+
writeFileSync(f.inputPath, JSON.stringify(f.proof, null, 2) + '\n');
|
|
6549
|
+
assert.throws(
|
|
6550
|
+
() =>
|
|
6551
|
+
readControlledRoutingProof(
|
|
6552
|
+
f.inputPath,
|
|
6553
|
+
f.candidateRoot,
|
|
6554
|
+
f.configPaths,
|
|
6555
|
+
),
|
|
6556
|
+
/receipt digest/,
|
|
6557
|
+
);
|
|
6558
|
+
},
|
|
6559
|
+
);
|
|
6560
|
+
|
|
6561
|
+
test(
|
|
6562
|
+
'independent ' +
|
|
6563
|
+
name +
|
|
6564
|
+
' config rejects stale bytes after opaque admission',
|
|
6565
|
+
(t) => {
|
|
6566
|
+
const f = controlledTunnelFixture(t);
|
|
6567
|
+
if (!supportsControlledRouting()) {
|
|
6568
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6569
|
+
return;
|
|
6570
|
+
}
|
|
6571
|
+
const handle = readControlledRoutingProof(
|
|
6572
|
+
f.inputPath,
|
|
6573
|
+
f.candidateRoot,
|
|
6574
|
+
f.configPaths,
|
|
6575
|
+
);
|
|
6576
|
+
writeFileSync(
|
|
6577
|
+
f.configPaths[name],
|
|
6578
|
+
JSON.stringify({
|
|
6579
|
+
ADMIN_PORTAL_PUBLIC_API_BASE_URL:
|
|
6580
|
+
'https://changed-8000.aue.devtunnels.ms',
|
|
6581
|
+
}),
|
|
6582
|
+
);
|
|
6583
|
+
assert.throws(
|
|
6584
|
+
() =>
|
|
6585
|
+
validateGeneratedWorkflow(
|
|
6586
|
+
f.fixture.manifest,
|
|
6587
|
+
f.fixture.workflow,
|
|
6588
|
+
generated,
|
|
6589
|
+
f.fixture.operation,
|
|
6590
|
+
handle,
|
|
6591
|
+
regularBytes(
|
|
6592
|
+
join(f.candidateRoot, '.eai/generated-source-operation.json'),
|
|
6593
|
+
),
|
|
6594
|
+
),
|
|
6595
|
+
/receipt digest/,
|
|
6596
|
+
);
|
|
6597
|
+
},
|
|
6598
|
+
);
|
|
6599
|
+
|
|
6600
|
+
for (const key of [
|
|
6601
|
+
'ADMIN_PORTAL_PUBLIC_API_BASE_URL',
|
|
6602
|
+
'ADMIN_PORTAL_WORKFLOW_CALLBACK_BASE_URL',
|
|
6603
|
+
]) {
|
|
6604
|
+
test(
|
|
6605
|
+
'independent ' +
|
|
6606
|
+
name +
|
|
6607
|
+
' config rejects wrong preferred ' +
|
|
6608
|
+
key +
|
|
6609
|
+
' despite recomputed digest',
|
|
6610
|
+
(t) => {
|
|
6611
|
+
const f = controlledTunnelFixture(t);
|
|
6612
|
+
if (!supportsControlledRouting()) {
|
|
6613
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6614
|
+
return;
|
|
6615
|
+
}
|
|
6616
|
+
const config = JSON.parse(regularBytes(f.configPaths[name]));
|
|
6617
|
+
config[key] = key.endsWith('API_BASE_URL')
|
|
6618
|
+
? 'https://other-8000.aue.devtunnels.ms'
|
|
6619
|
+
: 'https://other-3010.aue.devtunnels.ms';
|
|
6620
|
+
writeFileSync(f.configPaths[name], JSON.stringify(config));
|
|
6621
|
+
f.proof.sourceConfigSha256[name] = digest(
|
|
6622
|
+
regularBytes(f.configPaths[name]),
|
|
6623
|
+
);
|
|
6624
|
+
writeFileSync(f.inputPath, JSON.stringify(f.proof, null, 2) + '\n');
|
|
6625
|
+
assert.throws(
|
|
6626
|
+
() =>
|
|
6627
|
+
readControlledRoutingProof(
|
|
6628
|
+
f.inputPath,
|
|
6629
|
+
f.candidateRoot,
|
|
6630
|
+
f.configPaths,
|
|
6631
|
+
),
|
|
6632
|
+
/Independent.*destination/,
|
|
6633
|
+
);
|
|
6634
|
+
},
|
|
6635
|
+
);
|
|
6636
|
+
}
|
|
6637
|
+
}
|
|
6638
|
+
|
|
6639
|
+
test('controlled qualification cannot infer independent config paths from the proof', (t) => {
|
|
6640
|
+
const f = controlledTunnelFixture(t);
|
|
6641
|
+
if (!supportsControlledRouting()) {
|
|
6642
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6643
|
+
return;
|
|
6644
|
+
}
|
|
6645
|
+
assert.throws(
|
|
6646
|
+
() => readControlledRoutingProof(f.inputPath, f.candidateRoot),
|
|
6647
|
+
/explicit independent source-config paths/,
|
|
6648
|
+
);
|
|
6649
|
+
});
|
|
6650
|
+
|
|
6651
|
+
test('malformed protected config never exposes its private content in parser errors', (t) => {
|
|
6652
|
+
const f = controlledTunnelFixture(t);
|
|
6653
|
+
if (!supportsControlledRouting()) {
|
|
6654
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6655
|
+
return;
|
|
6656
|
+
}
|
|
6657
|
+
const bytes = Buffer.from('not-json-private-sentinel-do-not-emit');
|
|
6658
|
+
writeFileSync(f.configPaths.portal, bytes);
|
|
6659
|
+
f.proof.sourceConfigSha256.portal = digest(bytes);
|
|
6660
|
+
writeFileSync(f.inputPath, JSON.stringify(f.proof, null, 2) + '\n');
|
|
6661
|
+
assert.throws(
|
|
6662
|
+
() =>
|
|
6663
|
+
readControlledRoutingProof(f.inputPath, f.candidateRoot, f.configPaths),
|
|
6664
|
+
(error) =>
|
|
6665
|
+
error.message ===
|
|
6666
|
+
'Independent source-config receipt has invalid preferred routing fields.' &&
|
|
6667
|
+
!String(error).includes('private-sentinel'),
|
|
6668
|
+
);
|
|
6669
|
+
});
|
|
6670
|
+
|
|
6671
|
+
test('independent configs have a separate bounded limit without widening the proof cap', (t) => {
|
|
6672
|
+
const f = controlledTunnelFixture(t);
|
|
6673
|
+
if (!supportsControlledRouting()) {
|
|
6674
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6675
|
+
return;
|
|
6676
|
+
}
|
|
6677
|
+
const config = JSON.parse(regularBytes(f.configPaths.portal));
|
|
6678
|
+
config.controlledPadding = 'x'.repeat(9000);
|
|
6679
|
+
writeFileSync(f.configPaths.portal, JSON.stringify(config));
|
|
6680
|
+
f.proof.sourceConfigSha256.portal = digest(
|
|
6681
|
+
regularBytes(f.configPaths.portal),
|
|
6682
|
+
);
|
|
6683
|
+
writeFileSync(f.inputPath, JSON.stringify(f.proof, null, 2) + '\n');
|
|
6684
|
+
readControlledRoutingProof(f.inputPath, f.candidateRoot, f.configPaths);
|
|
6685
|
+
writeFileSync(f.configPaths.portal, 'x'.repeat(512 * 1024 + 1));
|
|
6686
|
+
assert.throws(
|
|
6687
|
+
() =>
|
|
6688
|
+
readControlledRoutingProof(f.inputPath, f.candidateRoot, f.configPaths),
|
|
6689
|
+
/bounded owner-only/,
|
|
6690
|
+
);
|
|
6691
|
+
});
|
|
6692
|
+
|
|
6693
|
+
test('opened controlled input rejects a swapped parent directory before accepting bytes', (t) => {
|
|
6694
|
+
const f = controlledTunnelFixture(t);
|
|
6695
|
+
if (!supportsControlledRouting()) {
|
|
6696
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6697
|
+
return;
|
|
6698
|
+
}
|
|
6699
|
+
const originalParent = dirname(f.inputPath);
|
|
6700
|
+
const movedParent = originalParent + '-moved';
|
|
6701
|
+
const hostileParent = realpathSync(
|
|
6702
|
+
mkdtempSync(join(tmpdir(), 'eai-controlled-parent-swap-')),
|
|
6703
|
+
);
|
|
6704
|
+
t.after(() => {
|
|
6705
|
+
rmSync(movedParent, { recursive: true, force: true });
|
|
6706
|
+
rmSync(hostileParent, { recursive: true, force: true });
|
|
6707
|
+
});
|
|
6708
|
+
writeFileSync(
|
|
6709
|
+
join(hostileParent, 'owner-routing.json'),
|
|
6710
|
+
regularBytes(f.inputPath),
|
|
6711
|
+
{ mode: 0o600 },
|
|
6712
|
+
);
|
|
6713
|
+
const program = `import assert from 'node:assert/strict';
|
|
6714
|
+
import {closeSync,constants as fsConstants,fstatSync,lstatSync,openSync as nativeOpen,readSync,realpathSync,renameSync,symlinkSync} from 'node:fs';
|
|
6715
|
+
import {dirname,isAbsolute,relative,resolve} from 'node:path';
|
|
6716
|
+
const openSync = (path, flags) => { renameSync(${JSON.stringify(originalParent)}, ${JSON.stringify(movedParent)}); symlinkSync(${JSON.stringify(hostileParent)}, ${JSON.stringify(originalParent)}); return nativeOpen(path, flags); };
|
|
6717
|
+
${controlledReadFlags.toString()}
|
|
6718
|
+
${snapshotControlledDirectories.toString()}
|
|
6719
|
+
${assertControlledDirectories.toString()}
|
|
6720
|
+
${controlledFileMatches.toString()}
|
|
6721
|
+
${readProtectedRoutingBytes.toString()}
|
|
6722
|
+
assert.throws(() => readProtectedRoutingBytes(${JSON.stringify(f.inputPath)}, ${JSON.stringify(f.candidateRoot)}), /parent chain changed/);`;
|
|
6723
|
+
const result = spawnSync(
|
|
6724
|
+
process.execPath,
|
|
6725
|
+
['--input-type=module', '-e', program],
|
|
6726
|
+
{ timeout: 2000, encoding: 'utf8' },
|
|
6727
|
+
);
|
|
6728
|
+
assert.equal(result.error, undefined);
|
|
6729
|
+
assert.equal(result.signal, null);
|
|
6730
|
+
assert.equal(result.status, 0, result.stderr);
|
|
6731
|
+
});
|
|
6732
|
+
|
|
6733
|
+
for (const alias of [false, true]) {
|
|
6734
|
+
test(
|
|
6735
|
+
'independent service config paths reject ' +
|
|
6736
|
+
(alias ? 'normalized aliases' : 'the same file'),
|
|
6737
|
+
(t) => {
|
|
6738
|
+
const f = controlledTunnelFixture(t);
|
|
6739
|
+
if (!supportsControlledRouting()) {
|
|
6740
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6741
|
+
return;
|
|
6742
|
+
}
|
|
6743
|
+
f.configPaths.portal = alias
|
|
6744
|
+
? dirname(f.configPaths.publicApi) + '/./public-api.json'
|
|
6745
|
+
: f.configPaths.publicApi;
|
|
6746
|
+
assert.throws(
|
|
6747
|
+
() =>
|
|
6748
|
+
readControlledRoutingProof(
|
|
6749
|
+
f.inputPath,
|
|
6750
|
+
f.candidateRoot,
|
|
6751
|
+
f.configPaths,
|
|
6752
|
+
),
|
|
6753
|
+
/distinct resolved paths/,
|
|
6754
|
+
);
|
|
6755
|
+
},
|
|
6756
|
+
);
|
|
6757
|
+
}
|
|
6758
|
+
|
|
6759
|
+
for (const [name, routing] of [
|
|
6760
|
+
[
|
|
6761
|
+
'hyphenated tunnel names',
|
|
6762
|
+
{
|
|
6763
|
+
evidenceBaseUrl: 'https://owner-run-name-8000.aue01.devtunnels.ms',
|
|
6764
|
+
callbackBaseUrl: 'https://owner-run-name-3010.aue.devtunnels.ms',
|
|
6765
|
+
},
|
|
6766
|
+
],
|
|
6767
|
+
[
|
|
6768
|
+
'a valid non-AUE tunnel region',
|
|
6769
|
+
{
|
|
6770
|
+
evidenceBaseUrl: 'https://owner-run-8000.use1.devtunnels.ms',
|
|
6771
|
+
callbackBaseUrl: 'https://owner-run-3010.use1.devtunnels.ms',
|
|
6772
|
+
},
|
|
6773
|
+
],
|
|
6774
|
+
]) {
|
|
6775
|
+
test('independently bound controlled inputs admit ' + name, (t) => {
|
|
6776
|
+
const f = controlledTunnelFixture(t, routing);
|
|
6777
|
+
if (!supportsControlledRouting()) {
|
|
6778
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6779
|
+
return;
|
|
6780
|
+
}
|
|
6781
|
+
assert.equal(
|
|
6782
|
+
resolveDeploymentWorkflowContext(f.candidateRoot, {
|
|
6783
|
+
controlledRoutingInputPath: f.inputPath,
|
|
6784
|
+
controlledRoutingConfigPaths: f.configPaths,
|
|
6785
|
+
}).context,
|
|
6786
|
+
'admin-portal-generated',
|
|
6787
|
+
);
|
|
6788
|
+
});
|
|
6789
|
+
}
|
|
6790
|
+
|
|
6791
|
+
for (const [name, routing] of [
|
|
6792
|
+
[
|
|
6793
|
+
'a hostile tunnel suffix',
|
|
6794
|
+
{
|
|
6795
|
+
evidenceBaseUrl:
|
|
6796
|
+
'https://owner-run-8000.use1.devtunnels.ms.attacker.example',
|
|
6797
|
+
},
|
|
6798
|
+
],
|
|
6799
|
+
[
|
|
6800
|
+
'an invalid tunnel region label',
|
|
6801
|
+
{ evidenceBaseUrl: 'https://owner-run-8000.bad_region.devtunnels.ms' },
|
|
6802
|
+
],
|
|
6803
|
+
[
|
|
6804
|
+
'a wrong evidence tunnel port',
|
|
6805
|
+
{ evidenceBaseUrl: 'https://owner-run-8001.use1.devtunnels.ms' },
|
|
6806
|
+
],
|
|
6807
|
+
[
|
|
6808
|
+
'a wrong callback tunnel port',
|
|
6809
|
+
{ callbackBaseUrl: 'https://owner-run-3000.use1.devtunnels.ms' },
|
|
6810
|
+
],
|
|
6811
|
+
[
|
|
6812
|
+
'a callback tunnel suffix',
|
|
6813
|
+
{
|
|
6814
|
+
callbackBaseUrl:
|
|
6815
|
+
'https://owner-run-3010.use1.devtunnels.ms.attacker.example',
|
|
6816
|
+
},
|
|
6817
|
+
],
|
|
6818
|
+
]) {
|
|
6819
|
+
test('even independently matching controlled configs reject ' + name, (t) => {
|
|
6820
|
+
const f = controlledTunnelFixture(t, routing);
|
|
6821
|
+
if (!supportsControlledRouting()) {
|
|
6822
|
+
assertControlledRoutingUnsupported(f.inputPath, f.candidateRoot);
|
|
6823
|
+
return;
|
|
6824
|
+
}
|
|
6825
|
+
assert.throws(
|
|
6826
|
+
() =>
|
|
6827
|
+
resolveDeploymentWorkflowContext(f.candidateRoot, {
|
|
6828
|
+
controlledRoutingInputPath: f.inputPath,
|
|
6829
|
+
controlledRoutingConfigPaths: f.configPaths,
|
|
6830
|
+
}),
|
|
6831
|
+
/port-(?:8000|3010) HTTPS tunnel/,
|
|
6832
|
+
);
|
|
6833
|
+
});
|
|
6834
|
+
}
|