productfoo 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +338 -2
- package/bin/foo +3 -0
- package/bin/foo.mjs +42 -0
- package/commands/bug.md +31 -0
- package/commands/discovery.md +28 -0
- package/commands/ideate.md +27 -0
- package/commands/init.md +28 -0
- package/commands/pitch.md +27 -0
- package/commands/preview.md +25 -0
- package/commands/research.md +31 -0
- package/commands/roast.md +27 -0
- package/commands/spec.md +30 -0
- package/config/pricing.yaml +198 -0
- package/config/routing-policy.yaml +171 -0
- package/dist-public/src/activeModel.d.ts +83 -0
- package/dist-public/src/activeModel.js +331 -0
- package/dist-public/src/agentAdapters.d.ts +122 -0
- package/dist-public/src/agentAdapters.js +178 -0
- package/dist-public/src/agentState.d.ts +43 -0
- package/dist-public/src/agentState.js +177 -0
- package/dist-public/src/bugCapsule.d.ts +94 -0
- package/dist-public/src/bugCapsule.js +227 -0
- package/dist-public/src/bugCapture.d.ts +26 -0
- package/dist-public/src/bugCapture.js +251 -0
- package/dist-public/src/bugOwnerLog.d.ts +93 -0
- package/dist-public/src/bugOwnerLog.js +304 -0
- package/dist-public/src/bugReportStore.d.ts +48 -0
- package/dist-public/src/bugReportStore.js +860 -0
- package/dist-public/src/bugSpecContext.d.ts +7 -0
- package/dist-public/src/bugSpecContext.js +129 -0
- package/dist-public/src/channelFreshness.d.ts +25 -0
- package/dist-public/src/channelFreshness.js +194 -0
- package/dist-public/src/cli.d.ts +30 -0
- package/dist-public/src/cli.js +1667 -0
- package/dist-public/src/cliArgs.d.ts +7 -0
- package/dist-public/src/cliArgs.js +44 -0
- package/dist-public/src/codeCatalog.d.ts +41 -0
- package/dist-public/src/codeCatalog.js +197 -0
- package/dist-public/src/commandContract.d.ts +84 -0
- package/dist-public/src/commandContract.js +390 -0
- package/dist-public/src/commandUsage.d.ts +3 -0
- package/dist-public/src/commandUsage.js +30 -0
- package/dist-public/src/commands/agent.d.ts +60 -0
- package/dist-public/src/commands/agent.js +1353 -0
- package/dist-public/src/commands/asp.d.ts +8 -0
- package/dist-public/src/commands/asp.js +669 -0
- package/dist-public/src/commands/evalCmd.d.ts +4 -0
- package/dist-public/src/commands/evalCmd.js +405 -0
- package/dist-public/src/commands/login.d.ts +25 -0
- package/dist-public/src/commands/login.js +94 -0
- package/dist-public/src/commands/loop.d.ts +16 -0
- package/dist-public/src/commands/loop.js +619 -0
- package/dist-public/src/commands/reconcile.d.ts +41 -0
- package/dist-public/src/commands/reconcile.js +292 -0
- package/dist-public/src/commands/runtime.d.ts +1 -0
- package/dist-public/src/commands/runtime.js +36 -0
- package/dist-public/src/commands/scanRedact.d.ts +8 -0
- package/dist-public/src/commands/scanRedact.js +266 -0
- package/dist-public/src/commands/setup.d.ts +28 -0
- package/dist-public/src/commands/setup.js +398 -0
- package/dist-public/src/commands/shared.d.ts +17 -0
- package/dist-public/src/commands/shared.js +147 -0
- package/dist-public/src/commands/source.d.ts +1 -0
- package/dist-public/src/commands/source.js +134 -0
- package/dist-public/src/commands/specCmd.d.ts +24 -0
- package/dist-public/src/commands/specCmd.js +794 -0
- package/dist-public/src/commands/update.d.ts +61 -0
- package/dist-public/src/commands/update.js +244 -0
- package/dist-public/src/comprehensionSignal.d.ts +20 -0
- package/dist-public/src/comprehensionSignal.js +166 -0
- package/dist-public/src/conductor/diversity.d.ts +48 -0
- package/dist-public/src/conductor/diversity.js +423 -0
- package/dist-public/src/conductor/engine.d.ts +7 -0
- package/dist-public/src/conductor/engine.js +295 -0
- package/dist-public/src/conductor/families/ideate.d.ts +2 -0
- package/dist-public/src/conductor/families/ideate.js +255 -0
- package/dist-public/src/conductor/families/interview.d.ts +2 -0
- package/dist-public/src/conductor/families/interview.js +39 -0
- package/dist-public/src/conductor/families/specSwarm.d.ts +2 -0
- package/dist-public/src/conductor/families/specSwarm.js +146 -0
- package/dist-public/src/conductor/familyDelivery.d.ts +6 -0
- package/dist-public/src/conductor/familyDelivery.js +37 -0
- package/dist-public/src/conductor/familyPackArtifact.d.ts +4 -0
- package/dist-public/src/conductor/familyPackArtifact.js +260 -0
- package/dist-public/src/conductor/ideateDecision.d.ts +6 -0
- package/dist-public/src/conductor/ideateDecision.js +237 -0
- package/dist-public/src/conductor/pack.d.ts +26 -0
- package/dist-public/src/conductor/pack.js +75 -0
- package/dist-public/src/conductor/types.d.ts +111 -0
- package/dist-public/src/conductor/types.js +7 -0
- package/dist-public/src/configPath.d.ts +2 -0
- package/dist-public/src/configPath.js +7 -0
- package/dist-public/src/discoveryArtifacts.d.ts +54 -0
- package/dist-public/src/discoveryArtifacts.js +467 -0
- package/dist-public/src/discoveryCommand.d.ts +127 -0
- package/dist-public/src/discoveryCommand.js +2148 -0
- package/dist-public/src/discoveryConnections.d.ts +339 -0
- package/dist-public/src/discoveryConnections.js +613 -0
- package/dist-public/src/discoveryDocs.d.ts +129 -0
- package/dist-public/src/discoveryDocs.js +577 -0
- package/dist-public/src/discoveryEvidence.d.ts +59 -0
- package/dist-public/src/discoveryEvidence.js +198 -0
- package/dist-public/src/discoveryFireflies.d.ts +25 -0
- package/dist-public/src/discoveryFireflies.js +551 -0
- package/dist-public/src/discoveryGmail.d.ts +12 -0
- package/dist-public/src/discoveryGmail.js +466 -0
- package/dist-public/src/discoveryGong.d.ts +12 -0
- package/dist-public/src/discoveryGong.js +491 -0
- package/dist-public/src/discoveryLearningRouting.d.ts +90 -0
- package/dist-public/src/discoveryLearningRouting.js +405 -0
- package/dist-public/src/discoveryMatching.d.ts +26 -0
- package/dist-public/src/discoveryMatching.js +73 -0
- package/dist-public/src/discoveryOutlook.d.ts +12 -0
- package/dist-public/src/discoveryOutlook.js +492 -0
- package/dist-public/src/discoveryProcess.d.ts +221 -0
- package/dist-public/src/discoveryProcess.js +703 -0
- package/dist-public/src/discoveryProof.d.ts +143 -0
- package/dist-public/src/discoveryProof.js +529 -0
- package/dist-public/src/discoveryProviders.d.ts +152 -0
- package/dist-public/src/discoveryProviders.js +756 -0
- package/dist-public/src/discoveryReadiness.d.ts +219 -0
- package/dist-public/src/discoveryReadiness.js +672 -0
- package/dist-public/src/discoveryReconcile.d.ts +57 -0
- package/dist-public/src/discoveryReconcile.js +424 -0
- package/dist-public/src/discoverySlack.d.ts +12 -0
- package/dist-public/src/discoverySlack.js +452 -0
- package/dist-public/src/discoverySources.d.ts +56 -0
- package/dist-public/src/discoverySources.js +322 -0
- package/dist-public/src/discoverySync.d.ts +52 -0
- package/dist-public/src/discoverySync.js +151 -0
- package/dist-public/src/discoverySynthesis.d.ts +57 -0
- package/dist-public/src/discoverySynthesis.js +215 -0
- package/dist-public/src/discoveryTeams.d.ts +12 -0
- package/dist-public/src/discoveryTeams.js +516 -0
- package/dist-public/src/discoveryZoom.d.ts +12 -0
- package/dist-public/src/discoveryZoom.js +523 -0
- package/dist-public/src/elicitationSurface.d.ts +13 -0
- package/dist-public/src/elicitationSurface.js +77 -0
- package/dist-public/src/errorReport.d.ts +99 -0
- package/dist-public/src/errorReport.js +443 -0
- package/dist-public/src/errorTaxonomy.d.ts +84 -0
- package/dist-public/src/errorTaxonomy.js +189 -0
- package/dist-public/src/evalCalibration.d.ts +82 -0
- package/dist-public/src/evalCalibration.js +169 -0
- package/dist-public/src/evalChecks.d.ts +21 -0
- package/dist-public/src/evalChecks.js +295 -0
- package/dist-public/src/evalCompile.d.ts +56 -0
- package/dist-public/src/evalCompile.js +58 -0
- package/dist-public/src/evalImprove.d.ts +60 -0
- package/dist-public/src/evalImprove.js +175 -0
- package/dist-public/src/evalJudge.d.ts +47 -0
- package/dist-public/src/evalJudge.js +143 -0
- package/dist-public/src/evalLabels.d.ts +100 -0
- package/dist-public/src/evalLabels.js +287 -0
- package/dist-public/src/evalRecord.d.ts +68 -0
- package/dist-public/src/evalRecord.js +165 -0
- package/dist-public/src/evalRoast.d.ts +88 -0
- package/dist-public/src/evalRoast.js +249 -0
- package/dist-public/src/evidenceIntake.d.ts +66 -0
- package/dist-public/src/evidenceIntake.js +356 -0
- package/dist-public/src/feedback.d.ts +78 -0
- package/dist-public/src/feedback.js +294 -0
- package/dist-public/src/firefliesClient.d.ts +106 -0
- package/dist-public/src/firefliesClient.js +258 -0
- package/dist-public/src/firstMoveTensions.d.ts +10 -0
- package/dist-public/src/firstMoveTensions.js +93 -0
- package/dist-public/src/gitRun.d.ts +21 -0
- package/dist-public/src/gitRun.js +50 -0
- package/dist-public/src/githubReconcile.d.ts +94 -0
- package/dist-public/src/githubReconcile.js +205 -0
- package/dist-public/src/gmailNango.d.ts +131 -0
- package/dist-public/src/gmailNango.js +329 -0
- package/dist-public/src/gongNango.d.ts +118 -0
- package/dist-public/src/gongNango.js +308 -0
- package/dist-public/src/grammarGate.d.ts +13 -0
- package/dist-public/src/grammarGate.js +72 -0
- package/dist-public/src/harnessGates.d.ts +65 -0
- package/dist-public/src/harnessGates.js +168 -0
- package/dist-public/src/humanSurfaceEval.d.ts +19 -0
- package/dist-public/src/humanSurfaceEval.js +156 -0
- package/dist-public/src/ideate.d.ts +43 -0
- package/dist-public/src/ideate.js +255 -0
- package/dist-public/src/ideateContext.d.ts +11 -0
- package/dist-public/src/ideateContext.js +83 -0
- package/dist-public/src/index.d.ts +2 -0
- package/dist-public/src/index.js +4 -0
- package/dist-public/src/initCommand.d.ts +25 -0
- package/dist-public/src/initCommand.js +558 -0
- package/dist-public/src/interviewGate.d.ts +25 -0
- package/dist-public/src/interviewGate.js +126 -0
- package/dist-public/src/jsonFile.d.ts +2 -0
- package/dist-public/src/jsonFile.js +22 -0
- package/dist-public/src/loopBudget.d.ts +182 -0
- package/dist-public/src/loopBudget.js +494 -0
- package/dist-public/src/loopContracts.d.ts +35 -0
- package/dist-public/src/loopContracts.js +364 -0
- package/dist-public/src/machineState.d.ts +1 -0
- package/dist-public/src/machineState.js +9 -0
- package/dist-public/src/notionNango.d.ts +93 -0
- package/dist-public/src/notionNango.js +279 -0
- package/dist-public/src/orchestrationResult.d.ts +11 -0
- package/dist-public/src/orchestrationResult.js +73 -0
- package/dist-public/src/outlookNango.d.ts +124 -0
- package/dist-public/src/outlookNango.js +327 -0
- package/dist-public/src/packetCorrections.d.ts +40 -0
- package/dist-public/src/packetCorrections.js +172 -0
- package/dist-public/src/packetGlossary.d.ts +6 -0
- package/dist-public/src/packetGlossary.js +42 -0
- package/dist-public/src/packetIndex.d.ts +54 -0
- package/dist-public/src/packetIndex.js +744 -0
- package/dist-public/src/packetIndexEntry.d.ts +3 -0
- package/dist-public/src/packetIndexEntry.js +128 -0
- package/dist-public/src/packetIndexServe.d.ts +13 -0
- package/dist-public/src/packetIndexServe.js +113 -0
- package/dist-public/src/packetIndexWorker.d.ts +7 -0
- package/dist-public/src/packetIndexWorker.js +45 -0
- package/dist-public/src/packetLifecycle.d.ts +30 -0
- package/dist-public/src/packetLifecycle.js +127 -0
- package/dist-public/src/packetReconcile.d.ts +73 -0
- package/dist-public/src/packetReconcile.js +386 -0
- package/dist-public/src/packetStatus.d.ts +125 -0
- package/dist-public/src/packetStatus.js +770 -0
- package/dist-public/src/packetValidation.d.ts +17 -0
- package/dist-public/src/packetValidation.js +333 -0
- package/dist-public/src/packetVerify.d.ts +73 -0
- package/dist-public/src/packetVerify.js +303 -0
- package/dist-public/src/packetWrite.d.ts +52 -0
- package/dist-public/src/packetWrite.js +448 -0
- package/dist-public/src/pitch.d.ts +38 -0
- package/dist-public/src/pitch.js +154 -0
- package/dist-public/src/processMemo.d.ts +8 -0
- package/dist-public/src/processMemo.js +23 -0
- package/dist-public/src/productDetect.d.ts +30 -0
- package/dist-public/src/productDetect.js +376 -0
- package/dist-public/src/profile.d.ts +62 -0
- package/dist-public/src/profile.js +334 -0
- package/dist-public/src/questionModel.d.ts +46 -0
- package/dist-public/src/questionModel.js +74 -0
- package/dist-public/src/questionText.d.ts +7 -0
- package/dist-public/src/questionText.js +38 -0
- package/dist-public/src/redactConfig.d.ts +2 -0
- package/dist-public/src/redactConfig.js +11 -0
- package/dist-public/src/redactEngine.d.ts +59 -0
- package/dist-public/src/redactEngine.js +509 -0
- package/dist-public/src/redactGate.d.ts +63 -0
- package/dist-public/src/redactGate.js +209 -0
- package/dist-public/src/repoRead.d.ts +29 -0
- package/dist-public/src/repoRead.js +160 -0
- package/dist-public/src/research.d.ts +242 -0
- package/dist-public/src/research.js +1314 -0
- package/dist-public/src/researchCompletion.d.ts +34 -0
- package/dist-public/src/researchCompletion.js +123 -0
- package/dist-public/src/researchExecution.d.ts +47 -0
- package/dist-public/src/researchExecution.js +309 -0
- package/dist-public/src/researchFormat.d.ts +21 -0
- package/dist-public/src/researchFormat.js +77 -0
- package/dist-public/src/researchGate.d.ts +86 -0
- package/dist-public/src/researchGate.js +1036 -0
- package/dist-public/src/researchReceipt.d.ts +76 -0
- package/dist-public/src/researchReceipt.js +148 -0
- package/dist-public/src/researchRender.d.ts +2 -0
- package/dist-public/src/researchRender.js +267 -0
- package/dist-public/src/researchSigningKey.d.ts +19 -0
- package/dist-public/src/researchSigningKey.js +95 -0
- package/dist-public/src/riskClassify.d.ts +42 -0
- package/dist-public/src/riskClassify.js +149 -0
- package/dist-public/src/routines.d.ts +42 -0
- package/dist-public/src/routines.js +269 -0
- package/dist-public/src/routingPolicy.d.ts +172 -0
- package/dist-public/src/routingPolicy.js +646 -0
- package/dist-public/src/secretInput.d.ts +8 -0
- package/dist-public/src/secretInput.js +89 -0
- package/dist-public/src/selfCheck.d.ts +7 -0
- package/dist-public/src/selfCheck.js +473 -0
- package/dist-public/src/selfCheckCache.d.ts +29 -0
- package/dist-public/src/selfCheckCache.js +147 -0
- package/dist-public/src/severityClass.d.ts +15 -0
- package/dist-public/src/severityClass.js +22 -0
- package/dist-public/src/skillClientPolicy.d.ts +8 -0
- package/dist-public/src/skillClientPolicy.js +16 -0
- package/dist-public/src/skillLocal.d.ts +98 -0
- package/dist-public/src/skillLocal.js +463 -0
- package/dist-public/src/skillTreeDigest.d.ts +9 -0
- package/dist-public/src/skillTreeDigest.js +64 -0
- package/dist-public/src/slackNango.d.ts +62 -0
- package/dist-public/src/slackNango.js +208 -0
- package/dist-public/src/social/arxiv.d.ts +19 -0
- package/dist-public/src/social/arxiv.js +96 -0
- package/dist-public/src/social/github.d.ts +2 -0
- package/dist-public/src/social/github.js +78 -0
- package/dist-public/src/social/hackernews.d.ts +2 -0
- package/dist-public/src/social/hackernews.js +67 -0
- package/dist-public/src/social/index.d.ts +10 -0
- package/dist-public/src/social/index.js +79 -0
- package/dist-public/src/social/reddit.d.ts +2 -0
- package/dist-public/src/social/reddit.js +125 -0
- package/dist-public/src/social/types.d.ts +73 -0
- package/dist-public/src/social/types.js +61 -0
- package/dist-public/src/social/xml.d.ts +10 -0
- package/dist-public/src/social/xml.js +49 -0
- package/dist-public/src/social/youtube.d.ts +7 -0
- package/dist-public/src/social/youtube.js +217 -0
- package/dist-public/src/sourceIngress.d.ts +488 -0
- package/dist-public/src/sourceIngress.js +1651 -0
- package/dist-public/src/specAfter.d.ts +187 -0
- package/dist-public/src/specAfter.js +814 -0
- package/dist-public/src/specBudget.d.ts +124 -0
- package/dist-public/src/specBudget.js +424 -0
- package/dist-public/src/specDecisionSignals.d.ts +38 -0
- package/dist-public/src/specDecisionSignals.js +121 -0
- package/dist-public/src/specLinkedCommits.d.ts +7 -0
- package/dist-public/src/specLinkedCommits.js +107 -0
- package/dist-public/src/specSections.d.ts +105 -0
- package/dist-public/src/specSections.js +718 -0
- package/dist-public/src/statusGlyphs.d.ts +18 -0
- package/dist-public/src/statusGlyphs.js +75 -0
- package/dist-public/src/swarmCommand.d.ts +10 -0
- package/dist-public/src/swarmCommand.js +96 -0
- package/dist-public/src/teamsNango.d.ts +114 -0
- package/dist-public/src/teamsNango.js +301 -0
- package/dist-public/src/tokenStore.d.ts +10 -0
- package/dist-public/src/tokenStore.js +77 -0
- package/dist-public/src/trustEnvelope.d.ts +78 -0
- package/dist-public/src/trustEnvelope.js +392 -0
- package/dist-public/src/untrustedText.d.ts +6 -0
- package/dist-public/src/untrustedText.js +20 -0
- package/dist-public/src/updateNotice.d.ts +41 -0
- package/dist-public/src/updateNotice.js +290 -0
- package/dist-public/src/vendorYaml.d.ts +1 -0
- package/dist-public/src/vendorYaml.js +7374 -0
- package/dist-public/src/workflowRunActuals.d.ts +67 -0
- package/dist-public/src/workflowRunActuals.js +184 -0
- package/dist-public/src/zendeskDiscovery.d.ts +12 -0
- package/dist-public/src/zendeskDiscovery.js +513 -0
- package/dist-public/src/zendeskNango.d.ts +131 -0
- package/dist-public/src/zendeskNango.js +344 -0
- package/dist-public/src/zoomNango.d.ts +106 -0
- package/dist-public/src/zoomNango.js +351 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/THIRD_PARTY_NOTICES.md +36 -0
- package/package.json +71 -4
- package/schemas/bug-capsule-receipt.schema.json +54 -0
- package/schemas/bug-capsule.schema.json +189 -0
- package/schemas/discovery-evidence-envelope.schema.json +79 -0
- package/schemas/discovery-processing-receipt.schema.json +71 -0
- package/schemas/discovery-service-contract.schema.json +189 -0
- package/skills/AGENTS.md +32 -0
- package/skills/README.md +287 -0
- package/skills/bug/SKILL.md +49 -0
- package/skills/build-preview-spec/SKILL.md +103 -0
- package/skills/capture-external-artifact/SKILL.md +23 -0
- package/skills/capture-external-artifact/agents/openai.yaml +4 -0
- package/skills/capture-external-artifact/references/extractor-libraries.md +27 -0
- package/skills/check-spec-health/SKILL.md +45 -0
- package/skills/check-spec-health/agents/openai.yaml +4 -0
- package/skills/competitive-positioning/SKILL.md +86 -0
- package/skills/competitive-positioning/agents/openai.yaml +4 -0
- package/skills/cpo-lens/SKILL.md +398 -0
- package/skills/cpo-lens/agents/openai.yaml +4 -0
- package/skills/create-prd/SKILL.md +26 -0
- package/skills/create-prd/agents/openai.yaml +4 -0
- package/skills/customer-discovery/SKILL.md +192 -0
- package/skills/design-system/SKILL.md +49 -0
- package/skills/design-system/agents/openai.yaml +4 -0
- package/skills/discussion-guide/SKILL.md +75 -0
- package/skills/discussion-guide/agents/openai.yaml +4 -0
- package/skills/firecrawl-research-papers/SKILL.md +96 -0
- package/skills/firecrawl-research-papers/agents/openai.yaml +4 -0
- package/skills/foo-roast/SKILL.md +325 -0
- package/skills/generate-gtm-brief/SKILL.md +41 -0
- package/skills/generate-gtm-brief/agents/openai.yaml +4 -0
- package/skills/generate-gtm-brief/references/gtm-brief-template.md +56 -0
- package/skills/gong-quotes/SKILL.md +43 -0
- package/skills/gong-quotes/agents/openai.yaml +4 -0
- package/skills/grill-me/SKILL.md +27 -0
- package/skills/grill-me/agents/openai.yaml +4 -0
- package/skills/ideate/SKILL.md +180 -0
- package/skills/ideate/references/family-packs.md +5 -0
- package/skills/initiative-tracker/SKILL.md +23 -0
- package/skills/initiative-tracker/agents/openai.yaml +4 -0
- package/skills/interop-review/SKILL.md +25 -0
- package/skills/interop-review/agents/openai.yaml +4 -0
- package/skills/interview-me/SKILL.md +404 -0
- package/skills/interview-me/agents/openai.yaml +4 -0
- package/skills/loop-library-audit/SKILL.md +123 -0
- package/skills/pitch/SKILL.md +110 -0
- package/skills/pitch/references/opportunity-assessment.md +136 -0
- package/skills/pitch/references/prfaq-template.md +54 -0
- package/skills/pmm-release-briefing/SKILL.md +27 -0
- package/skills/pmm-release-briefing/agents/openai.yaml +4 -0
- package/skills/pmm-release-briefing/references/variant-generalized.md +16 -0
- package/skills/positioning-messaging/SKILL.md +70 -0
- package/skills/positioning-messaging/agents/openai.yaml +4 -0
- package/skills/positioning-messaging/references/guest-insights.md +1400 -0
- package/skills/preview-swarm/SKILL.md +418 -0
- package/skills/preview-swarm/agents/openai.yaml +4 -0
- package/skills/preview-swarm/references/html-effectiveness-build-previews.md +168 -0
- package/skills/product-copywriting/SKILL.md +127 -0
- package/skills/product-copywriting/agents/openai.yaml +4 -0
- package/skills/product-copywriting/references/foo-voice.md +26 -0
- package/skills/product-shape-interview/SKILL.md +219 -0
- package/skills/product-shape-interview/agents/openai.yaml +4 -0
- package/skills/ready-for-release/SKILL.md +39 -0
- package/skills/ready-for-release/agents/openai.yaml +4 -0
- package/skills/redact-transcripts/SKILL.md +23 -0
- package/skills/redact-transcripts/agents/openai.yaml +4 -0
- package/skills/research-brief/SKILL.md +48 -0
- package/skills/research-brief/agents/openai.yaml +4 -0
- package/skills/research-refresh/SKILL.md +63 -0
- package/skills/research-refresh/references/worked-cases.md +44 -0
- package/skills/review-loop/SKILL.md +200 -0
- package/skills/review-spec/SKILL.md +32 -0
- package/skills/review-spec/agents/openai.yaml +4 -0
- package/skills/rfc-from-research/SKILL.md +42 -0
- package/skills/rfc-from-research/agents/openai.yaml +4 -0
- package/skills/rfc-from-research/references/rfc-pattern.md +69 -0
- package/skills/skill-registry.yaml +810 -0
- package/skills/spec-cleanup/SKILL.md +103 -0
- package/skills/spec-cleanup/agents/openai.yaml +4 -0
- package/skills/spec-swarm/SKILL.md +87 -0
- package/skills/spec-swarm/agents/openai.yaml +4 -0
- package/skills/spec-swarm/template/SKILL.template.md +94 -0
- package/skills/spec-swarm/template/personas/accessibility-inclusion.md +40 -0
- package/skills/spec-swarm/template/personas/ai-technical-pm.md +69 -0
- package/skills/spec-swarm/template/personas/analytics-engineering.md +39 -0
- package/skills/spec-swarm/template/personas/applied-ai-eval.md +64 -0
- package/skills/spec-swarm/template/personas/business-gm-pm.md +67 -0
- package/skills/spec-swarm/template/personas/consumer-craft-pm.md +68 -0
- package/skills/spec-swarm/template/personas/content-design.md +40 -0
- package/skills/spec-swarm/template/personas/cpo.md +42 -0
- package/skills/spec-swarm/template/personas/data-science.md +42 -0
- package/skills/spec-swarm/template/personas/developer-experience.md +65 -0
- package/skills/spec-swarm/template/personas/dx-product.md +70 -0
- package/skills/spec-swarm/template/personas/field.md +43 -0
- package/skills/spec-swarm/template/personas/finance-cfo.md +40 -0
- package/skills/spec-swarm/template/personas/growth-pm.md +67 -0
- package/skills/spec-swarm/template/personas/platform-pm.md +67 -0
- package/skills/spec-swarm/template/personas/pmm.md +43 -0
- package/skills/spec-swarm/template/personas/principal-product-designer.md +42 -0
- package/skills/spec-swarm/template/personas/product-ops-program.md +41 -0
- package/skills/spec-swarm/template/personas/qa-release.md +41 -0
- package/skills/spec-swarm/template/personas/responsible-ai-safety.md +64 -0
- package/skills/spec-swarm/template/personas/security-privacy-legal.md +40 -0
- package/skills/spec-swarm/template/personas/strategy-competitive.md +41 -0
- package/skills/spec-swarm/template/personas/technical-architect.md +42 -0
- package/skills/spec-swarm/template/personas/technical-lead.md +43 -0
- package/skills/spec-swarm/template/personas/user-research.md +41 -0
- package/skills/spec-swarm-accessibility-inclusion/SKILL.md +95 -0
- package/skills/spec-swarm-accessibility-inclusion/agents/openai.yaml +4 -0
- package/skills/spec-swarm-ai-technical-pm/SKILL.md +124 -0
- package/skills/spec-swarm-analytics-engineering/SKILL.md +94 -0
- package/skills/spec-swarm-analytics-engineering/agents/openai.yaml +4 -0
- package/skills/spec-swarm-applied-ai-eval/SKILL.md +120 -0
- package/skills/spec-swarm-business-gm-pm/SKILL.md +122 -0
- package/skills/spec-swarm-consumer-craft-pm/SKILL.md +122 -0
- package/skills/spec-swarm-content-design/SKILL.md +95 -0
- package/skills/spec-swarm-content-design/agents/openai.yaml +4 -0
- package/skills/spec-swarm-cpo/SKILL.md +94 -0
- package/skills/spec-swarm-cpo/agents/openai.yaml +4 -0
- package/skills/spec-swarm-data-science/SKILL.md +95 -0
- package/skills/spec-swarm-data-science/agents/openai.yaml +4 -0
- package/skills/spec-swarm-developer-experience/SKILL.md +121 -0
- package/skills/spec-swarm-dx-product/SKILL.md +126 -0
- package/skills/spec-swarm-field/SKILL.md +96 -0
- package/skills/spec-swarm-field/agents/openai.yaml +4 -0
- package/skills/spec-swarm-finance-cfo/SKILL.md +95 -0
- package/skills/spec-swarm-finance-cfo/agents/openai.yaml +4 -0
- package/skills/spec-swarm-growth-pm/SKILL.md +122 -0
- package/skills/spec-swarm-platform-pm/SKILL.md +122 -0
- package/skills/spec-swarm-pmm/SKILL.md +96 -0
- package/skills/spec-swarm-pmm/agents/openai.yaml +4 -0
- package/skills/spec-swarm-principal-product-designer/SKILL.md +95 -0
- package/skills/spec-swarm-principal-product-designer/agents/openai.yaml +4 -0
- package/skills/spec-swarm-product-ops-program/SKILL.md +96 -0
- package/skills/spec-swarm-product-ops-program/agents/openai.yaml +4 -0
- package/skills/spec-swarm-qa-release/SKILL.md +96 -0
- package/skills/spec-swarm-qa-release/agents/openai.yaml +4 -0
- package/skills/spec-swarm-responsible-ai-safety/SKILL.md +120 -0
- package/skills/spec-swarm-security-privacy-legal/SKILL.md +95 -0
- package/skills/spec-swarm-security-privacy-legal/agents/openai.yaml +4 -0
- package/skills/spec-swarm-strategy-competitive/SKILL.md +96 -0
- package/skills/spec-swarm-strategy-competitive/agents/openai.yaml +4 -0
- package/skills/spec-swarm-technical-architect/SKILL.md +95 -0
- package/skills/spec-swarm-technical-architect/agents/openai.yaml +4 -0
- package/skills/spec-swarm-technical-lead/SKILL.md +96 -0
- package/skills/spec-swarm-technical-lead/agents/openai.yaml +4 -0
- package/skills/spec-swarm-user-research/SKILL.md +95 -0
- package/skills/spec-swarm-user-research/agents/openai.yaml +4 -0
- package/skills/strategy-check/SKILL.md +170 -0
- package/skills/strategy-check/agents/openai.yaml +4 -0
- package/skills/study-digest/SKILL.md +41 -0
- package/skills/synthetic-customer/SKILL.md +68 -0
- package/skills/update-cli-spec/SKILL.md +69 -0
- package/skills/update-cli-spec/agents/openai.yaml +4 -0
- package/skills/voc-researcher/SKILL.md +45 -0
- package/skills/voc-researcher/agents/openai.yaml +4 -0
- package/skills-lock.json +35 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
// Provider-neutral readiness and bounded metadata listing
|
|
2
|
+
// (nango-discovery-connector-v0 T-003, R-004/R-011/R-013).
|
|
3
|
+
//
|
|
4
|
+
// Before reading meetings, every host can call a provider-neutral readiness
|
|
5
|
+
// operation; inside an approved study or inbox scope the service may list
|
|
6
|
+
// only the completed metadata the existing strict matcher needs. Everything
|
|
7
|
+
// on a surface this module produces is content-free: opaque references,
|
|
8
|
+
// observed capability booleans, state names, digests, and signed references.
|
|
9
|
+
// No provider note, summary, raw identifier, credential, response body,
|
|
10
|
+
// query string, fragment, or unrecognized field ever rides through.
|
|
11
|
+
//
|
|
12
|
+
// Two seams carry the hosted/provider split (foo-mcp-server-v0 is unbuilt —
|
|
13
|
+
// hosted tiers stay contract_only, D-012):
|
|
14
|
+
//
|
|
15
|
+
// DiscoveryReadinessService — the hosted state this module reports but
|
|
16
|
+
// never fabricates: service reachability (unreachability throws the T-002
|
|
17
|
+
// error type), the hosted connection record, the target's service grant
|
|
18
|
+
// with its operation count, and the tenant-scoped canonical connection key.
|
|
19
|
+
//
|
|
20
|
+
// DiscoveryListingSource<TRaw> — the provider side: observed per-connection
|
|
21
|
+
// capabilities (never asserted from a fixture) and a bounded listing of raw
|
|
22
|
+
// records the provider adapter normalizes. Implementations exist per
|
|
23
|
+
// provider; the conformance suite runs ONE contract against Granola and a
|
|
24
|
+
// structurally different fake provider (V-006).
|
|
25
|
+
//
|
|
26
|
+
// The signed-and-encrypted session_ref, the hosted source fingerprint, and
|
|
27
|
+
// the service policy are real code here, not seams: the hosted service
|
|
28
|
+
// supplies the server-held key, and everything else about the references —
|
|
29
|
+
// self-contained binding, opacity, expiry, tamper-evidence — is enforced in
|
|
30
|
+
// this module because it is the contract, not the transport.
|
|
31
|
+
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "node:crypto";
|
|
32
|
+
import { DiscoveryServiceUnreachableError, recheckDiscoveryAccessBoundary, validateDiscoveryServiceGrant, } from "./discoveryConnections.js";
|
|
33
|
+
import { sanitizeDiscoveryStableUrl, validateNormalizedSession, } from "./discoverySources.js";
|
|
34
|
+
export const READINESS_SCHEMA_VERSION = "0.1";
|
|
35
|
+
// The wire contract this module speaks. A listing source built for a older
|
|
36
|
+
// contract version is reported as `adapter_stale` with the exact repair
|
|
37
|
+
// command (R-013) — it is never silently invoked.
|
|
38
|
+
export const READINESS_CONTRACT_VERSION = "discovery-contract-0.1";
|
|
39
|
+
// --- Versioned service policy (R-005) ----------------------------------------
|
|
40
|
+
// Hard ceilings carried as versioned readiness fields so a later change is
|
|
41
|
+
// explicit rather than silently host-dependent. The values are the ones the
|
|
42
|
+
// Gong local worker already proves in-repo (gongNango.ts GONG_POLICY); this
|
|
43
|
+
// constant is the provider-neutral hosted form of the same policy.
|
|
44
|
+
export const DISCOVERY_SERVICE_POLICY = {
|
|
45
|
+
policy_version: "discovery-service-0.1",
|
|
46
|
+
max_response_bytes: 8 * 1024 * 1024,
|
|
47
|
+
fetch_timeout_ms: 90_000,
|
|
48
|
+
max_model_input_tokens: 100_000,
|
|
49
|
+
// Bound on one listing. A source's own page/window ceilings apply first;
|
|
50
|
+
// when the bound would be exceeded the listing reports truncation instead
|
|
51
|
+
// of widening the read (R-004/R-010).
|
|
52
|
+
max_list_sessions: 50,
|
|
53
|
+
};
|
|
54
|
+
// A listed session_ref expires quickly: it exists so a host can hand back
|
|
55
|
+
// exactly one bounded read, not to seed a long-lived handle. Never longer
|
|
56
|
+
// than the grant it was minted under.
|
|
57
|
+
export const SESSION_REF_TTL_MS = 15 * 60_000;
|
|
58
|
+
let failureCounter = 0;
|
|
59
|
+
function newTraceId() {
|
|
60
|
+
failureCounter += 1;
|
|
61
|
+
return `tr:${randomBytes(8).toString("hex")}${failureCounter.toString(16).padStart(4, "0")}`;
|
|
62
|
+
}
|
|
63
|
+
function failure(code, message, resume_action) {
|
|
64
|
+
return { code, message, trace_id: newTraceId(), resume_action };
|
|
65
|
+
}
|
|
66
|
+
// One typed outcome per boundary reason: the status stays inside the closed
|
|
67
|
+
// service-state union, the failure code carries the vocabulary, and the
|
|
68
|
+
// resume action is exact. Exhaustive over DiscoveryBoundaryFailureReason, so
|
|
69
|
+
// a new reason in discoveryConnections breaks compilation here instead of
|
|
70
|
+
// silently mapping to a vague refusal.
|
|
71
|
+
const BOUNDARY_FAILURES = {
|
|
72
|
+
unknown_grant: { status: "needs_service_grant", code: "approval_missing", message: "no active service grant exists for this target", resume: "Register and re-confirm the target to mint a grant." },
|
|
73
|
+
subject_mismatch: { status: "invalid", code: "service_bug", message: "the authenticated subject is not the grant's subject", resume: "Retry with the target's own authenticated subject." },
|
|
74
|
+
connection_mismatch: { status: "invalid", code: "service_bug", message: "the connection reference does not match the grant", resume: "Retry with the target's own connection reference." },
|
|
75
|
+
workspace_mismatch: { status: "invalid", code: "service_bug", message: "the workspace reference does not match the grant", resume: "Retry with the target's own workspace reference." },
|
|
76
|
+
target_mismatch: { status: "invalid", code: "service_bug", message: "the discovery target does not match the grant", resume: "Retry with the registered target reference." },
|
|
77
|
+
lane_mismatch: { status: "invalid", code: "service_bug", message: "the discovery lane does not match the grant", resume: "Retry with the registered lane." },
|
|
78
|
+
approval_mismatch: { status: "needs_source_approval", code: "approval_missing", message: "the local source approval no longer matches the service grant", resume: "Run the local resume command from needs_source_approval, then re-register and re-confirm the target." },
|
|
79
|
+
stale_revocation_epoch: { status: "revoked", code: "approval_missing", message: "the local source was revoked after this grant was minted; the grant is dead", resume: "Re-approve the source and re-register the target if hosted reads are wanted again." },
|
|
80
|
+
grant_expired: { status: "needs_service_grant", code: "approval_missing", message: "the service grant has expired", resume: "Register and re-confirm the target to mint a fresh grant." },
|
|
81
|
+
selector_out_of_scope: { status: "invalid", code: "approval_missing", message: "the requested window or selector is outside the grant's approved scope", resume: "Narrow the request to the approved selectors and retry." },
|
|
82
|
+
budget_exhausted: { status: "budget_exhausted", code: "approval_missing", message: "the grant's operation budget is exhausted for its window", resume: "A person authorizes a fresh budget; productfoo never silently exceeds one." },
|
|
83
|
+
};
|
|
84
|
+
export function boundaryFailureFor(reason) {
|
|
85
|
+
return BOUNDARY_FAILURES[reason];
|
|
86
|
+
}
|
|
87
|
+
const SERVICE_RESULT_KEYS = new Set(["schema_version", "operation", "status", "errors", "handoff", "next_actions", "coverage"]);
|
|
88
|
+
// Closure assertion for every result this module returns: no variant may grow
|
|
89
|
+
// extra top-level fields here. Operation-specific payloads (capabilities,
|
|
90
|
+
// sessions, refs) are carried inside the fields the packet names for the op —
|
|
91
|
+
// readiness's `capabilities`, listing's `sessions` — declared below.
|
|
92
|
+
function closedResult(result) {
|
|
93
|
+
for (const key of Object.keys(result)) {
|
|
94
|
+
if (!SERVICE_RESULT_KEYS.has(key) && key !== "capabilities" && key !== "connection_ref" && key !== "target_ref" && key !== "sessions") {
|
|
95
|
+
throw new Error(`discovery service result carries a field outside the closed contract: ${key}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function commonResult(operation, status, options = {}) {
|
|
101
|
+
return closedResult({
|
|
102
|
+
schema_version: READINESS_SCHEMA_VERSION,
|
|
103
|
+
operation,
|
|
104
|
+
status,
|
|
105
|
+
errors: [],
|
|
106
|
+
handoff: null,
|
|
107
|
+
next_actions: [],
|
|
108
|
+
coverage: {},
|
|
109
|
+
...options,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function errorOf(f) {
|
|
113
|
+
return { code: f.code, message: f.message, trace_id: f.trace_id };
|
|
114
|
+
}
|
|
115
|
+
// Derives the two domain-separated keys the hosted service holds from one
|
|
116
|
+
// root secret. The root never leaves the service; the derived keys never
|
|
117
|
+
// appear on any surface this module produces.
|
|
118
|
+
export function deriveDiscoveryServiceKeys(root_key) {
|
|
119
|
+
const root = Buffer.from(root_key, "utf8");
|
|
120
|
+
return {
|
|
121
|
+
source_fingerprint_key: createHmac("sha256", root).update("foo_discovery_source_fingerprint_key/v1").digest("base64"),
|
|
122
|
+
session_ref_key: createHmac("sha256", root).update("foo_discovery_session_ref_key/v1").digest(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// The hosted source fingerprint grammar: `hmac-sha256:v1:<hex>` over the
|
|
126
|
+
// canonical {foo_subject, provider, canonical_connection_key,
|
|
127
|
+
// provider_session_identity} with a server-held domain-separated key. It is
|
|
128
|
+
// derived from identity, never from transcript text, titles, participants,
|
|
129
|
+
// URLs, or host aliases (R-004).
|
|
130
|
+
export function hostedDiscoverySourceFingerprint(input, fingerprint_key) {
|
|
131
|
+
for (const [field, value] of [
|
|
132
|
+
["subject", input.subject_ref],
|
|
133
|
+
["provider", input.provider],
|
|
134
|
+
["canonical connection key", input.canonical_connection_key],
|
|
135
|
+
["provider session identity", input.provider_session_identity],
|
|
136
|
+
]) {
|
|
137
|
+
if (!value?.trim())
|
|
138
|
+
throw new Error(`hosted source fingerprint requires a ${field}`);
|
|
139
|
+
}
|
|
140
|
+
const mac = createHmac("sha256", fingerprint_key).update(["foo_discovery_source_fingerprint/v1", input.subject_ref, input.provider.trim().toLowerCase(), input.canonical_connection_key, input.provider_session_identity].join("\n"));
|
|
141
|
+
return `hmac-sha256:v1:${mac.digest("hex")}`;
|
|
142
|
+
}
|
|
143
|
+
const SESSION_REF_PREFIX = "sref:v1:";
|
|
144
|
+
const SESSION_REF_PAYLOAD_KEYS = new Set([
|
|
145
|
+
"kind",
|
|
146
|
+
"version",
|
|
147
|
+
"subject_ref",
|
|
148
|
+
"canonical_connection_key",
|
|
149
|
+
"provider",
|
|
150
|
+
"target_ref",
|
|
151
|
+
"grant_digest",
|
|
152
|
+
"provider_session_identity",
|
|
153
|
+
"hosted_source_fingerprint",
|
|
154
|
+
"revocation_epoch",
|
|
155
|
+
"expires_at",
|
|
156
|
+
]);
|
|
157
|
+
// Authenticated encryption (AES-256-GCM with a server-held key): the payload
|
|
158
|
+
// is unreadable without the key and tampering with any byte breaks the tag.
|
|
159
|
+
function sealDiscoverySessionRef(payload, key) {
|
|
160
|
+
for (const key_of_payload of Object.keys(payload)) {
|
|
161
|
+
if (!SESSION_REF_PAYLOAD_KEYS.has(key_of_payload))
|
|
162
|
+
throw new Error(`session_ref payload carries an unknown field: ${key_of_payload}`);
|
|
163
|
+
}
|
|
164
|
+
if (payload.kind !== "discovery_session_ref" || payload.version !== "v1")
|
|
165
|
+
throw new Error("session_ref payload must be a v1 discovery_session_ref");
|
|
166
|
+
if (!Number.isInteger(payload.revocation_epoch) || payload.revocation_epoch < 0)
|
|
167
|
+
throw new Error("session_ref payload revocation_epoch must be a non-negative integer");
|
|
168
|
+
if (!Number.isFinite(Date.parse(payload.expires_at)))
|
|
169
|
+
throw new Error("session_ref payload expires_at must be an ISO timestamp");
|
|
170
|
+
const nonce = randomBytes(12);
|
|
171
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
172
|
+
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(payload), "utf8"), cipher.final()]);
|
|
173
|
+
return SESSION_REF_PREFIX + Buffer.concat([nonce, cipher.getAuthTag(), ciphertext]).toString("base64url");
|
|
174
|
+
}
|
|
175
|
+
// Service-side verification. A ref that fails for ANY reason opens nothing —
|
|
176
|
+
// the reasons are for service logs and traces, never for the host.
|
|
177
|
+
export function openDiscoverySessionRef(session_ref, key, now) {
|
|
178
|
+
if (!session_ref.startsWith(SESSION_REF_PREFIX))
|
|
179
|
+
return { ok: false, reason: "not_a_session_ref" };
|
|
180
|
+
let raw;
|
|
181
|
+
try {
|
|
182
|
+
raw = Buffer.from(session_ref.slice(SESSION_REF_PREFIX.length), "base64url");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return { ok: false, reason: "malformed" };
|
|
186
|
+
}
|
|
187
|
+
if (raw.length < 12 + 16 + 2)
|
|
188
|
+
return { ok: false, reason: "malformed" };
|
|
189
|
+
const nonce = raw.subarray(0, 12);
|
|
190
|
+
const tag = raw.subarray(12, 28);
|
|
191
|
+
const ciphertext = raw.subarray(28);
|
|
192
|
+
let plaintext;
|
|
193
|
+
try {
|
|
194
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
195
|
+
decipher.setAuthTag(tag);
|
|
196
|
+
plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Wrong keys and tampered bytes are indistinguishable by design (GCM).
|
|
200
|
+
return { ok: false, reason: "tampered" };
|
|
201
|
+
}
|
|
202
|
+
let parsed;
|
|
203
|
+
try {
|
|
204
|
+
parsed = JSON.parse(plaintext.toString("utf8"));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { ok: false, reason: "malformed" };
|
|
208
|
+
}
|
|
209
|
+
if (!parsed || typeof parsed !== "object")
|
|
210
|
+
return { ok: false, reason: "malformed" };
|
|
211
|
+
for (const field of Object.keys(parsed)) {
|
|
212
|
+
if (!SESSION_REF_PAYLOAD_KEYS.has(field))
|
|
213
|
+
return { ok: false, reason: "malformed" };
|
|
214
|
+
}
|
|
215
|
+
const payload = parsed;
|
|
216
|
+
if (payload.kind !== "discovery_session_ref" || payload.version !== "v1")
|
|
217
|
+
return { ok: false, reason: "malformed" };
|
|
218
|
+
if (Date.parse(now) > Date.parse(payload.expires_at))
|
|
219
|
+
return { ok: false, reason: "expired" };
|
|
220
|
+
return { ok: true, payload };
|
|
221
|
+
}
|
|
222
|
+
// One probe of both sides, typed. Service unreachability is honest and
|
|
223
|
+
// specific: nothing is verified, so nothing is claimed (needs_service_
|
|
224
|
+
// verification), and the failure code is `network`.
|
|
225
|
+
async function probeConnection(input, deps) {
|
|
226
|
+
try {
|
|
227
|
+
const hosted = await deps.service.connectionState({ subject_ref: input.subject_ref, connection_ref: input.connection_ref });
|
|
228
|
+
const observed = await deps.listing.observeConnection();
|
|
229
|
+
return { ok: true, hosted, observed };
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error instanceof DiscoveryServiceUnreachableError) {
|
|
233
|
+
const f = failure("network", "the productfoo discovery service is unreachable; hosted connection state could not be verified", "Retry when the productfoo discovery service is reachable; nothing was read and nothing was written.");
|
|
234
|
+
return { ok: false, failure: f, status: "needs_service_verification" };
|
|
235
|
+
}
|
|
236
|
+
const f = failure("service_bug", "the readiness probe failed inside the productfoo service; the failure was recorded for its trace id", "Retry the readiness operation; if it repeats, report the trace id to productfoo.");
|
|
237
|
+
return { ok: false, failure: f, status: "invalid" };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function observedCaps(connection) {
|
|
241
|
+
return connection.state === "connected" ? connection.capabilities : null;
|
|
242
|
+
}
|
|
243
|
+
// Observed, never asserted: `verified_live` cannot be produced here even if a
|
|
244
|
+
// registry claims it and the observation is clean — the verified-live state is
|
|
245
|
+
// minted by the governed registry after the T-008 dogfood bar, not by code.
|
|
246
|
+
function supportStateFor(claimed, caps) {
|
|
247
|
+
if (!caps.list_sessions || !caps.open_transcript)
|
|
248
|
+
return "unavailable";
|
|
249
|
+
if (!caps.stable_link || !caps.timestamps)
|
|
250
|
+
return "degraded";
|
|
251
|
+
return claimed === "verified_live" ? "contract_only" : claimed;
|
|
252
|
+
}
|
|
253
|
+
function readinessHandoff(status, observed) {
|
|
254
|
+
switch (status) {
|
|
255
|
+
case "not_connected":
|
|
256
|
+
return { kind: "provider_connect", requires_human: true };
|
|
257
|
+
case "auth_expired":
|
|
258
|
+
return { kind: "provider_reconnect", requires_human: true };
|
|
259
|
+
case "needs_provider_access":
|
|
260
|
+
// Preserve the sanitized provider surface when the observation carries
|
|
261
|
+
// one (a stable link), per the packet's human workflow step 4.
|
|
262
|
+
return { kind: "provider_access_request", requires_human: true };
|
|
263
|
+
default:
|
|
264
|
+
void observed;
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// The readiness operation: report productfoo-service reachability, the authenticated
|
|
269
|
+
// connection state, observed capabilities, server-side-synthesis
|
|
270
|
+
// availability, target-local source-approval and service-grant states, and
|
|
271
|
+
// scheduler/budget state. It never lists meetings and never returns provider
|
|
272
|
+
// payloads. The status is the first blocker in a fixed order; the full report
|
|
273
|
+
// rides in coverage.readiness either way.
|
|
274
|
+
export async function discoverConnectionReady(input, deps) {
|
|
275
|
+
const report = {
|
|
276
|
+
service_reachable: true,
|
|
277
|
+
capability_source: "observed",
|
|
278
|
+
connection_state: "not_connected",
|
|
279
|
+
support_state: "unavailable",
|
|
280
|
+
synthesis: { available: Boolean(input.synthesis?.available) },
|
|
281
|
+
approval: { state: "unregistered", matches_grant: null },
|
|
282
|
+
grant: { state: "unregistered" },
|
|
283
|
+
scheduler: { mode: "manual", armed: false },
|
|
284
|
+
policy: { ...DISCOVERY_SERVICE_POLICY },
|
|
285
|
+
};
|
|
286
|
+
const common = { operation: "discovery_connection_ready", connection_ref: input.connection_ref, ...(input.target_ref ? { target_ref: input.target_ref } : {}) };
|
|
287
|
+
if (!input.subject_ref?.trim() || !input.connection_ref?.trim() || !Number.isFinite(Date.parse(input.now))) {
|
|
288
|
+
const f = failure("service_bug", "the readiness request is malformed; nothing was probed", "Retry with an authenticated subject, a connection reference, and the current time.");
|
|
289
|
+
return {
|
|
290
|
+
...commonResult("discovery_connection_ready", "invalid", { errors: [errorOf(f)], next_actions: [f.resume_action], coverage: { readiness: report } }),
|
|
291
|
+
...common,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const probe = await probeConnection({ subject_ref: input.subject_ref, connection_ref: input.connection_ref }, deps);
|
|
295
|
+
if (!probe.ok) {
|
|
296
|
+
return {
|
|
297
|
+
...commonResult("discovery_connection_ready", probe.status, {
|
|
298
|
+
errors: [errorOf(probe.failure)],
|
|
299
|
+
next_actions: [probe.failure.resume_action],
|
|
300
|
+
handoff: probe.handoff ?? null,
|
|
301
|
+
coverage: { readiness: { ...report, service_reachable: probe.failure.code !== "network" } },
|
|
302
|
+
}),
|
|
303
|
+
...common,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const caps = observedCaps(probe.observed);
|
|
307
|
+
const hostedState = probe.hosted.state;
|
|
308
|
+
const observedState = probe.observed.state;
|
|
309
|
+
// Compose the authenticated connection state honestly: the worse of the
|
|
310
|
+
// hosted record and the provider observation wins, revoked worst of all.
|
|
311
|
+
const connection_state = hostedState === "revoked" || observedState === "revoked"
|
|
312
|
+
? "revoked"
|
|
313
|
+
: hostedState === "auth_expired" || observedState === "auth_expired"
|
|
314
|
+
? "auth_expired"
|
|
315
|
+
: hostedState === "not_connected" || observedState === "not_connected"
|
|
316
|
+
? "not_connected"
|
|
317
|
+
: "connected";
|
|
318
|
+
report.connection_state = connection_state;
|
|
319
|
+
const finish = (status, extra = {}) => closedResult({ ...commonResult("discovery_connection_ready", status, extra), ...common, coverage: { ...extra.coverage, readiness: report } });
|
|
320
|
+
if (connection_state === "revoked") {
|
|
321
|
+
const f = failure("not_connected", "the connection was revoked; a new connection must be created before any hosted read", "Reconnect the provider through productfoo's connect page to create a new connection.");
|
|
322
|
+
return finish("revoked", { errors: [errorOf(f)], next_actions: [f.resume_action], handoff: { kind: "provider_connect", requires_human: true } });
|
|
323
|
+
}
|
|
324
|
+
if (connection_state === "auth_expired") {
|
|
325
|
+
const f = failure("auth_expired", "the connection's provider authorization expired or was withdrawn", "A person reconnects the provider through productfoo; a non-interactive agent relays this and stops.");
|
|
326
|
+
return finish("auth_expired", { errors: [errorOf(f)], next_actions: [f.resume_action], handoff: readinessHandoff("auth_expired", probe.observed) });
|
|
327
|
+
}
|
|
328
|
+
if (connection_state === "not_connected") {
|
|
329
|
+
const f = failure("not_connected", "no provider connection is registered for this subject", "A person connects the provider through productfoo's connect landing page; a non-interactive agent relays this and stops.");
|
|
330
|
+
return finish("not_connected", { errors: [errorOf(f)], next_actions: [f.resume_action], handoff: readinessHandoff("not_connected", probe.observed) });
|
|
331
|
+
}
|
|
332
|
+
if (!caps) {
|
|
333
|
+
// Connected on both sides but the observation carries no capabilities —
|
|
334
|
+
// an adapter that cannot describe the connection is stale by definition.
|
|
335
|
+
const f = failure("adapter_stale", "the provider adapter did not report an observable capability set", `Upgrade the provider adapter to a build declaring ${READINESS_CONTRACT_VERSION}.`);
|
|
336
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
337
|
+
}
|
|
338
|
+
// C-006: missing required capabilities degrade honestly and stop the flow.
|
|
339
|
+
if (!caps.list_sessions) {
|
|
340
|
+
report.support_state = "unavailable";
|
|
341
|
+
const f = failure("capability_missing", "the provider connection cannot list completed sessions, so no metadata read is possible", "Connect the provider through a connector that supports bounded session listing, or ask the provider for one.");
|
|
342
|
+
return finish("needs_provider_connector", { errors: [errorOf(f)], next_actions: [f.resume_action], handoff: { kind: "provider_connector_missing", requires_human: true } });
|
|
343
|
+
}
|
|
344
|
+
if (!caps.open_transcript) {
|
|
345
|
+
report.support_state = "unavailable";
|
|
346
|
+
const f = failure("capability_missing", "the provider connection has no transcript access, so server-side synthesis cannot run; metadata listing still works", "Request full transcript access for the connection, then rerun readiness.");
|
|
347
|
+
return finish("needs_provider_access", { errors: [errorOf(f)], next_actions: [f.resume_action], handoff: readinessHandoff("needs_provider_access", probe.observed) });
|
|
348
|
+
}
|
|
349
|
+
report.support_state = supportStateFor(deps.listing.claimed_support_state, caps);
|
|
350
|
+
// Target-local approval and service-grant states: reported whenever a
|
|
351
|
+
// target is bound; they gate the status only for a bound target.
|
|
352
|
+
if (input.target_ref) {
|
|
353
|
+
const local = input.local;
|
|
354
|
+
if (!local) {
|
|
355
|
+
const f = failure("service_bug", "a bound target was presented without the local approval state the boundary recheck requires", "Retry with the current local approval digest and revocation epoch for this target.");
|
|
356
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
357
|
+
}
|
|
358
|
+
let lookup;
|
|
359
|
+
try {
|
|
360
|
+
lookup = await deps.service.grantForTarget({ subject_ref: input.subject_ref, target_ref: input.target_ref });
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (error instanceof DiscoveryServiceUnreachableError) {
|
|
364
|
+
const f = failure("network", "the productfoo discovery service is unreachable; the target's service-grant state could not be verified", "Retry when the productfoo discovery service is reachable; nothing was read and nothing was written.");
|
|
365
|
+
return finish("needs_service_verification", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
366
|
+
}
|
|
367
|
+
const f = failure("service_bug", "the grant lookup failed inside the productfoo service; the failure was recorded for its trace id", "Retry the readiness operation; if it repeats, report the trace id to productfoo.");
|
|
368
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
369
|
+
}
|
|
370
|
+
if (!lookup.grant) {
|
|
371
|
+
report.approval = { state: local.approval_digest ? "approved" : "missing", matches_grant: null };
|
|
372
|
+
report.grant = { state: "missing" };
|
|
373
|
+
const f = failure("approval_missing", "no active service grant exists for this target; hosted reads stay locked", "Register the target manifest and have the same person confirm it out of band to mint a grant.");
|
|
374
|
+
return finish("needs_service_grant", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
375
|
+
}
|
|
376
|
+
const check = recheckDiscoveryAccessBoundary({
|
|
377
|
+
grant: lookup.grant,
|
|
378
|
+
expected: {
|
|
379
|
+
subject_ref: input.subject_ref,
|
|
380
|
+
connection_ref: input.connection_ref,
|
|
381
|
+
workspace_ref: local.workspace_ref,
|
|
382
|
+
target_id: local.target_id,
|
|
383
|
+
lane: local.lane,
|
|
384
|
+
approval_digest: local.approval_digest ?? "",
|
|
385
|
+
revocation_epoch: local.revocation_epoch,
|
|
386
|
+
},
|
|
387
|
+
operations_used: lookup.operations_used,
|
|
388
|
+
now: input.now,
|
|
389
|
+
});
|
|
390
|
+
const grant = lookup.grant;
|
|
391
|
+
report.grant = {
|
|
392
|
+
state: check.ok ? "active" : check.reason === "grant_expired" ? "expired" : check.reason === "stale_revocation_epoch" ? "stale_revocation_epoch" : check.reason === "approval_mismatch" ? "approval_mismatch" : "missing",
|
|
393
|
+
expires_at: grant.expires_at,
|
|
394
|
+
budget: { max_operations: grant.budget.max_operations, used: lookup.operations_used, window: grant.budget.window },
|
|
395
|
+
};
|
|
396
|
+
report.approval = { state: local.approval_digest ? "approved" : "missing", matches_grant: check.ok ? true : check.reason === "approval_mismatch" ? false : null };
|
|
397
|
+
if (!check.ok && check.reason === "approval_mismatch") {
|
|
398
|
+
const f = failure("approval_missing", "the local source approval no longer matches the service grant; approve the current scope and register the target again", "Run the local resume command from needs_source_approval, then re-register and re-confirm the target.");
|
|
399
|
+
return finish("needs_source_approval", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
400
|
+
}
|
|
401
|
+
if (!check.ok && check.reason === "stale_revocation_epoch") {
|
|
402
|
+
const f = failure("approval_missing", "the local source was revoked after this grant was minted; the grant is dead", "Re-approve the source and re-register the target if hosted reads are wanted again.");
|
|
403
|
+
return finish("revoked", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
404
|
+
}
|
|
405
|
+
if (!check.ok && check.reason === "grant_expired") {
|
|
406
|
+
const f = failure("approval_missing", "the service grant has expired", "Register and re-confirm the target to mint a fresh grant.");
|
|
407
|
+
return finish("needs_service_grant", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
408
|
+
}
|
|
409
|
+
if (!check.ok && check.reason === "budget_exhausted") {
|
|
410
|
+
const f = failure("approval_missing", "the grant's operation budget is exhausted for its window", "A person authorizes a fresh budget; productfoo never silently exceeds one.");
|
|
411
|
+
return finish("budget_exhausted", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
412
|
+
}
|
|
413
|
+
if (!check.ok) {
|
|
414
|
+
const f = failure("service_bug", "the service grant does not match the presented target identity", "Retry with the target's own subject, workspace, and connection references.");
|
|
415
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else if (input.local?.approval_digest != null) {
|
|
419
|
+
report.approval = { state: "approved", matches_grant: null };
|
|
420
|
+
}
|
|
421
|
+
else if (input.local) {
|
|
422
|
+
report.approval = { state: "missing", matches_grant: null };
|
|
423
|
+
}
|
|
424
|
+
const caveats = [];
|
|
425
|
+
if (!caps.stable_link || !caps.timestamps) {
|
|
426
|
+
caveats.push(!caps.stable_link ? "no stable provider link is available for this connection" : "timestamp locators are not available for this connection");
|
|
427
|
+
}
|
|
428
|
+
if (report.support_state === "degraded")
|
|
429
|
+
caveats.push("the connection reports degraded capability coverage");
|
|
430
|
+
return finish("ready", {
|
|
431
|
+
capabilities: { ...caps },
|
|
432
|
+
next_actions: [
|
|
433
|
+
...caveats.map((caveat) => `Degraded capability: ${caveat}.`),
|
|
434
|
+
...(input.target_ref ? ["Run the bounded listing inside the approved scope when a match is wanted."] : ["Register and confirm a target manifest before any hosted read."]),
|
|
435
|
+
],
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const LISTED_SESSION_KEYS = new Set([
|
|
439
|
+
"schema_version",
|
|
440
|
+
"provider",
|
|
441
|
+
"connection_alias",
|
|
442
|
+
"completed_at",
|
|
443
|
+
"completion_state",
|
|
444
|
+
"title",
|
|
445
|
+
"participants",
|
|
446
|
+
"stable_url",
|
|
447
|
+
"capabilities",
|
|
448
|
+
"access",
|
|
449
|
+
"session_ref",
|
|
450
|
+
]);
|
|
451
|
+
const FORBIDDEN_LISTED_KEY = /transcript|content|note|summary|runtime_handle|token|secret|credential|password|nango|query|fragment/i;
|
|
452
|
+
const EMAIL_SHAPE = /[\w.+-]+@[\w-]+\.[\w.-]+/;
|
|
453
|
+
// Privacy-minimized participant labels (R-004): the strict matcher compares
|
|
454
|
+
// names, so names survive; anything email-shaped collapses to its @domain
|
|
455
|
+
// label and the local part never leaves the service.
|
|
456
|
+
function minimizeParticipantLabel(label) {
|
|
457
|
+
const trimmed = label.trim();
|
|
458
|
+
if (!trimmed)
|
|
459
|
+
return null;
|
|
460
|
+
if (EMAIL_SHAPE.test(trimmed)) {
|
|
461
|
+
const at = trimmed.lastIndexOf("@");
|
|
462
|
+
return at > 0 && at < trimmed.length - 1 ? `@${trimmed.slice(at + 1).toLowerCase()}` : null;
|
|
463
|
+
}
|
|
464
|
+
return trimmed;
|
|
465
|
+
}
|
|
466
|
+
function buildListedSession(session, session_ref, transcript_access) {
|
|
467
|
+
const listed = {
|
|
468
|
+
schema_version: "0.1",
|
|
469
|
+
provider: session.provider,
|
|
470
|
+
connection_alias: session.connection_alias,
|
|
471
|
+
completed_at: session.completed_at,
|
|
472
|
+
completion_state: "completed",
|
|
473
|
+
title: session.title,
|
|
474
|
+
participants: [...new Set(session.participants.map(minimizeParticipantLabel).filter((label) => label !== null))],
|
|
475
|
+
capabilities: { ...session.capabilities },
|
|
476
|
+
access: transcript_access ? "available" : "denied",
|
|
477
|
+
session_ref,
|
|
478
|
+
};
|
|
479
|
+
if (session.stable_url)
|
|
480
|
+
listed.stable_url = sanitizeDiscoveryStableUrl(session.provider, session.stable_url);
|
|
481
|
+
for (const key of Object.keys(listed)) {
|
|
482
|
+
if (!LISTED_SESSION_KEYS.has(key))
|
|
483
|
+
throw new Error(`listed session carries a field outside the closed contract: ${key}`);
|
|
484
|
+
if (FORBIDDEN_LISTED_KEY.test(key))
|
|
485
|
+
throw new Error(`listed session carries a forbidden field: ${key}`);
|
|
486
|
+
}
|
|
487
|
+
return listed;
|
|
488
|
+
}
|
|
489
|
+
// The bounded listing operation: inside an approved study or inbox scope,
|
|
490
|
+
// list only completed metadata the existing strict matcher needs, each entry
|
|
491
|
+
// carrying a signed session_ref instead of a provider handle. Every gate
|
|
492
|
+
// fails closed BEFORE any provider call; an empty bounded listing is
|
|
493
|
+
// `awaiting_match` with `sessions: []`, not an error.
|
|
494
|
+
export async function discoverListSessions(input, deps) {
|
|
495
|
+
const common = { operation: "discovery_list_sessions", connection_ref: input.connection_ref, target_ref: input.target_ref };
|
|
496
|
+
const finish = (status, extra = {}) => closedResult({ ...commonResult("discovery_list_sessions", status, extra), ...common, sessions: extra.sessions ?? [] });
|
|
497
|
+
const shapeOk = input.subject_ref?.trim() &&
|
|
498
|
+
input.connection_ref?.trim() &&
|
|
499
|
+
input.target_ref?.trim() &&
|
|
500
|
+
input.local?.approval_digest &&
|
|
501
|
+
Number.isFinite(Date.parse(input.now)) &&
|
|
502
|
+
Number.isFinite(Date.parse(input.window?.from)) &&
|
|
503
|
+
Number.isFinite(Date.parse(input.window?.to)) &&
|
|
504
|
+
Date.parse(input.window.from) < Date.parse(input.window.to);
|
|
505
|
+
if (!shapeOk) {
|
|
506
|
+
const f = failure("service_bug", "the listing request is malformed or its window is empty; nothing was probed", "Retry with opaque references, the current local approval state, and a bounded non-empty window.");
|
|
507
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
508
|
+
}
|
|
509
|
+
// Gate 1 — hosted connection state (zero provider calls before it passes).
|
|
510
|
+
let hosted;
|
|
511
|
+
try {
|
|
512
|
+
hosted = await deps.service.connectionState({ subject_ref: input.subject_ref, connection_ref: input.connection_ref });
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
if (error instanceof DiscoveryServiceUnreachableError) {
|
|
516
|
+
const f = failure("network", "the productfoo discovery service is unreachable; hosted connection state could not be verified", "Retry when the productfoo discovery service is reachable; nothing was read and nothing was written.");
|
|
517
|
+
return finish("needs_service_verification", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
518
|
+
}
|
|
519
|
+
const f = failure("service_bug", "the connection lookup failed inside the productfoo service; the failure was recorded for its trace id", "Retry the listing; if it repeats, report the trace id to productfoo.");
|
|
520
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
521
|
+
}
|
|
522
|
+
if (hosted.state === "revoked") {
|
|
523
|
+
const f = failure("not_connected", "the connection was revoked; no hosted read is possible", "Reconnect the provider through productfoo to create a new connection.");
|
|
524
|
+
return finish("revoked", { errors: [errorOf(f)], next_actions: [f.resume_action], handoff: { kind: "provider_connect", requires_human: true } });
|
|
525
|
+
}
|
|
526
|
+
if (hosted.state === "auth_expired") {
|
|
527
|
+
const f = failure("auth_expired", "the connection's provider authorization expired or was withdrawn", "A person reconnects the provider through productfoo; a non-interactive agent relays this and stops.");
|
|
528
|
+
return finish("auth_expired", { errors: [errorOf(f)], next_actions: [f.resume_action], handoff: { kind: "provider_reconnect", requires_human: true } });
|
|
529
|
+
}
|
|
530
|
+
if (hosted.state === "not_connected" || !hosted.state) {
|
|
531
|
+
const f = failure("not_connected", "no provider connection is registered for this subject", "A person connects the provider through productfoo's connect landing page; a non-interactive agent relays this and stops.");
|
|
532
|
+
return finish("not_connected", { errors: [errorOf(f)], next_actions: [f.resume_action], handoff: { kind: "provider_connect", requires_human: true } });
|
|
533
|
+
}
|
|
534
|
+
// Gate 2 — the service grant for this target, then the full boundary
|
|
535
|
+
// recheck against the CURRENT local ledger state.
|
|
536
|
+
let lookup;
|
|
537
|
+
try {
|
|
538
|
+
lookup = await deps.service.grantForTarget({ subject_ref: input.subject_ref, target_ref: input.target_ref });
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
const f = failure("network", "the productfoo discovery service is unreachable; the target's service-grant state could not be verified", "Retry when the productfoo discovery service is reachable; nothing was read and nothing was written.");
|
|
542
|
+
return finish("needs_service_verification", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
543
|
+
}
|
|
544
|
+
if (!lookup.grant) {
|
|
545
|
+
const f = failure("approval_missing", "no active service grant exists for this target; hosted reads stay locked", "Register the target manifest and have the same person confirm it out of band to mint a grant.");
|
|
546
|
+
return finish("needs_service_grant", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
547
|
+
}
|
|
548
|
+
const grantCheck = validateDiscoveryServiceGrant(lookup.grant);
|
|
549
|
+
if (!grantCheck.ok) {
|
|
550
|
+
const f = failure("service_bug", "the stored service grant is not a valid closed grant", "Report the trace id to productfoo; nothing was read.");
|
|
551
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
552
|
+
}
|
|
553
|
+
const grant = grantCheck.grant;
|
|
554
|
+
const boundary = recheckDiscoveryAccessBoundary({
|
|
555
|
+
grant,
|
|
556
|
+
expected: {
|
|
557
|
+
subject_ref: input.subject_ref,
|
|
558
|
+
connection_ref: input.connection_ref,
|
|
559
|
+
workspace_ref: input.local.workspace_ref,
|
|
560
|
+
target_id: input.local.target_id,
|
|
561
|
+
lane: input.local.lane,
|
|
562
|
+
approval_digest: input.local.approval_digest,
|
|
563
|
+
revocation_epoch: input.local.revocation_epoch,
|
|
564
|
+
},
|
|
565
|
+
operations_used: lookup.operations_used,
|
|
566
|
+
now: input.now,
|
|
567
|
+
});
|
|
568
|
+
if (!boundary.ok) {
|
|
569
|
+
const mappedFailure = boundaryFailureFor(boundary.reason);
|
|
570
|
+
const f = failure(mappedFailure.code, mappedFailure.message, mappedFailure.resume);
|
|
571
|
+
return finish(mappedFailure.status, { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
572
|
+
}
|
|
573
|
+
// Gate 3 — observed capabilities. Listing is the metadata read; a missing
|
|
574
|
+
// transcript capability degrades the entries (access: "denied") instead of
|
|
575
|
+
// blocking the bounded listing, and the readiness operation is where the
|
|
576
|
+
// needs_provider_access stop lives (C-006).
|
|
577
|
+
let observed;
|
|
578
|
+
try {
|
|
579
|
+
observed = await deps.listing.observeConnection();
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
const f = failure("provider_failure", "the provider connection could not be observed; nothing was read", "Retry the listing; if it repeats, report the trace id to productfoo.");
|
|
583
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
584
|
+
}
|
|
585
|
+
if (observed.state !== "connected") {
|
|
586
|
+
const f = failure("not_connected", "the provider connection is no longer usable for listing", "Reconnect or refresh the provider connection through productfoo, then retry.");
|
|
587
|
+
return finish(observed.state === "revoked" ? "revoked" : observed.state === "auth_expired" ? "auth_expired" : "not_connected", {
|
|
588
|
+
errors: [errorOf(f)],
|
|
589
|
+
next_actions: [f.resume_action],
|
|
590
|
+
handoff: { kind: "provider_connect", requires_human: true },
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
if (!observed.capabilities.list_sessions) {
|
|
594
|
+
const f = failure("capability_missing", "the provider connection cannot list completed sessions, so no metadata read is possible", "Connect the provider through a connector that supports bounded session listing, or ask the provider for one.");
|
|
595
|
+
return finish("needs_provider_connector", { errors: [errorOf(f)], next_actions: [f.resume_action], handoff: { kind: "provider_connector_missing", requires_human: true } });
|
|
596
|
+
}
|
|
597
|
+
// The read itself: one bounded call inside the granted window. Provider
|
|
598
|
+
// errors are typed and content-free — the underlying message (which could
|
|
599
|
+
// carry a provider body) is never forwarded.
|
|
600
|
+
let listed;
|
|
601
|
+
try {
|
|
602
|
+
listed = await deps.listing.listWithin({ window: input.window, limit: DISCOVERY_SERVICE_POLICY.max_list_sessions });
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
if (error instanceof DiscoveryServiceUnreachableError) {
|
|
606
|
+
const f = failure("network", "the provider could not be reached for the bounded listing; nothing was read", "Retry when the provider is reachable; nothing was written.");
|
|
607
|
+
return finish("needs_service_verification", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
608
|
+
}
|
|
609
|
+
const f = failure("provider_failure", "the provider listing failed inside the bounded read; nothing was read", "Retry the listing; if it repeats, report the trace id to productfoo.");
|
|
610
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
611
|
+
}
|
|
612
|
+
if (listed.truncated) {
|
|
613
|
+
const f = failure("provider_failure", "the approved window holds more sessions than the bounded listing allows", "Narrow the approved window or selectors and list again; productfoo never widens a window and never auto-retries.");
|
|
614
|
+
return finish("budget_exhausted", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
615
|
+
}
|
|
616
|
+
// Normalize through the provider adapter, keep only completed sessions.
|
|
617
|
+
const sessions = [];
|
|
618
|
+
for (const record of listed.records) {
|
|
619
|
+
let normalized;
|
|
620
|
+
try {
|
|
621
|
+
normalized = deps.listing.adapter.normalizeSession(record, deps.listing.connection_alias);
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
const f = failure("service_bug", "a provider record could not be normalized into the closed session shape", "Report the trace id to productfoo; nothing was read.");
|
|
625
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
626
|
+
}
|
|
627
|
+
if (normalized.completion_state !== "completed")
|
|
628
|
+
continue;
|
|
629
|
+
const problems = validateNormalizedSession(normalized);
|
|
630
|
+
if (problems.length) {
|
|
631
|
+
const f = failure("service_bug", "a normalized session failed the closed validation; nothing was exposed", "Report the trace id to productfoo; nothing was read.");
|
|
632
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
633
|
+
}
|
|
634
|
+
if (!normalized.runtime_handle?.trim()) {
|
|
635
|
+
const f = failure("service_bug", "a normalized session lacks the private session identity needed to mint its reference", "Report the trace id to productfoo; nothing was read.");
|
|
636
|
+
return finish("invalid", { errors: [errorOf(f)], next_actions: [f.resume_action] });
|
|
637
|
+
}
|
|
638
|
+
const hosted_fingerprint = hostedDiscoverySourceFingerprint({ subject_ref: input.subject_ref, provider: normalized.provider, canonical_connection_key: hosted.canonical_connection_key, provider_session_identity: normalized.runtime_handle }, deps.keys.source_fingerprint_key);
|
|
639
|
+
const expires_at = new Date(Math.min(Date.parse(input.now) + SESSION_REF_TTL_MS, Date.parse(grant.expires_at))).toISOString();
|
|
640
|
+
const sealed = sealDiscoverySessionRef({
|
|
641
|
+
kind: "discovery_session_ref",
|
|
642
|
+
version: "v1",
|
|
643
|
+
subject_ref: input.subject_ref,
|
|
644
|
+
canonical_connection_key: hosted.canonical_connection_key,
|
|
645
|
+
provider: normalized.provider,
|
|
646
|
+
target_ref: input.target_ref,
|
|
647
|
+
grant_digest: grant.approval_digest,
|
|
648
|
+
provider_session_identity: normalized.runtime_handle,
|
|
649
|
+
hosted_source_fingerprint: hosted_fingerprint,
|
|
650
|
+
revocation_epoch: input.local.revocation_epoch,
|
|
651
|
+
expires_at,
|
|
652
|
+
}, deps.keys.session_ref_key);
|
|
653
|
+
sessions.push(buildListedSession(normalized, sealed, observed.capabilities.open_transcript));
|
|
654
|
+
}
|
|
655
|
+
if (sessions.length === 0) {
|
|
656
|
+
// An empty bounded listing is `awaiting_match` with `sessions: []`, never
|
|
657
|
+
// an error (Contracts section).
|
|
658
|
+
return finish("awaiting_match", { next_actions: ["No completed sessions were listed inside the approved window; widen nothing — wait for completed sessions or adjust the approved scope."], coverage: { window: input.window, listed_records: listed.records.length, sessions: 0 } });
|
|
659
|
+
}
|
|
660
|
+
return finish("ready", {
|
|
661
|
+
coverage: {
|
|
662
|
+
window: input.window,
|
|
663
|
+
listed_records: listed.records.length,
|
|
664
|
+
sessions: sessions.length,
|
|
665
|
+
support_state: supportStateFor(deps.listing.claimed_support_state, observed.capabilities),
|
|
666
|
+
transcript_access: observed.capabilities.open_transcript ? "granted" : "missing",
|
|
667
|
+
policy_version: DISCOVERY_SERVICE_POLICY.policy_version,
|
|
668
|
+
session_ref_ttl_ms: SESSION_REF_TTL_MS,
|
|
669
|
+
},
|
|
670
|
+
sessions,
|
|
671
|
+
});
|
|
672
|
+
}
|