@cohortapp/agent-sdk 2.17.0 → 2.18.4
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/.claude/settings.json +18 -0
- package/.env.example +18 -5
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +89 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +78 -2
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +13 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +51 -37
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +595 -149
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +24 -0
- package/scripts/daemon/responder.mjs +246 -79
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/context/budget.test.mjs +0 -252
- package/lib/context/history-scope.test.mjs +0 -79
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -908
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -406
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -554
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -556
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -221
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -1,1268 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tool-surface.test.mjs — the curated org tool table + shared executor.
|
|
3
|
-
*
|
|
4
|
-
* Hermetic: every network call rides an injected fetchImpl; the send-gate is an
|
|
5
|
-
* injected stub; env is snapshotted/cleared so a developer's COHORT_* vars
|
|
6
|
-
* can't leak in. Run: node --test lib/org/tool-surface.test.mjs
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
"use strict";
|
|
10
|
-
|
|
11
|
-
import { test, before, after } from "node:test";
|
|
12
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import assert from "node:assert/strict";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
ORG_TOOLS,
|
|
19
|
-
getOrgTools,
|
|
20
|
-
isOrgTool,
|
|
21
|
-
orgToolDef,
|
|
22
|
-
OUTBOUND_METHODS,
|
|
23
|
-
emailFamilyAvailable,
|
|
24
|
-
artifactFamilyAvailable,
|
|
25
|
-
deskFamilyAvailable,
|
|
26
|
-
resolveOrgToolConfig,
|
|
27
|
-
executeOrgTool,
|
|
28
|
-
DEFAULT_COHORT_BASE,
|
|
29
|
-
} from "./tool-surface.mjs";
|
|
30
|
-
import { methodDef, PROTOCOL_VERSION, READS } from "./protocol.mjs";
|
|
31
|
-
import { ADMIN_TOOLS, expectedAccessFor, normalizeToolAccess } from "./tool-access.mjs";
|
|
32
|
-
|
|
33
|
-
// ── env hygiene ────────────────────────────────────────────────────────────
|
|
34
|
-
const ENV_KEYS = [
|
|
35
|
-
"COHORT_API_TOKEN", "COHORT_TOKEN", "COHORT_API_KEY", "COHORT_ORG_ID",
|
|
36
|
-
"COHORT_BASE", "COHORT_API_URL", "COHORT_AGENT_ROOT", "AGENT_ROOT",
|
|
37
|
-
"COHORT_AGENT_ID", "COHORT_AGENT_EMAIL",
|
|
38
|
-
];
|
|
39
|
-
const saved = {};
|
|
40
|
-
before(() => {
|
|
41
|
-
for (const k of ENV_KEYS) { saved[k] = process.env[k]; delete process.env[k]; }
|
|
42
|
-
});
|
|
43
|
-
after(() => {
|
|
44
|
-
for (const k of ENV_KEYS) {
|
|
45
|
-
if (saved[k] === undefined) delete process.env[k];
|
|
46
|
-
else process.env[k] = saved[k];
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// ── helpers ────────────────────────────────────────────────────────────────
|
|
51
|
-
const CFG = { org: { cohort: { enabled: true, base: "https://org.example", orgId: "acme", token: "nlk_secret" } } };
|
|
52
|
-
|
|
53
|
-
/** fetch stub: records calls, returns a res-frame body. */
|
|
54
|
-
function fakeFetch(body = { ok: true, result: { fine: true } }, { status = 200, ok = true } = {}) {
|
|
55
|
-
const calls = [];
|
|
56
|
-
const fn = async (url, init) => {
|
|
57
|
-
calls.push({ url: String(url), init: init || {} });
|
|
58
|
-
return { ok, status, json: async () => body, headers: { get: () => undefined } };
|
|
59
|
-
};
|
|
60
|
-
fn.calls = calls;
|
|
61
|
-
return fn;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ── table shape ────────────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
test("table: curated 54 + 5 email + 5 artifact + 77 desk tools, snake_case names, valid access + schemas", () => {
|
|
67
|
-
// 2026-08 mobile-parity delta: +5 mail (report_spam/react/move_mailbox/
|
|
68
|
-
// summarise/ask) and +3 asks (files/calendar/crm) → desk 56 → 64; the
|
|
69
|
-
// call-working-sessions delta adds the 5 calls-desk tools → 69.
|
|
70
|
-
// The wire-contract fix adds task_assign: hq's editTaskSchema (board.updateTask)
|
|
71
|
-
// has NO assignee field, so reassignment needs board.assignTask — curated 28 → 29.
|
|
72
|
-
// The conversation-parity delta adds the per-message actions the human
|
|
73
|
-
// long-press sheet ships (react/bookmark/delete/mark_unread_from) and the two
|
|
74
|
-
// huddle verbs the share tools presuppose (start/join) — curated 29 → 35.
|
|
75
|
-
// They are ALWAYS-ON, not desk-gated: the base messaging/channel/calling
|
|
76
|
-
// families have been vendored since SP3.
|
|
77
|
-
// The macro-parity delta adds org_pulse (GET ops?pulse=1 — the founder's
|
|
78
|
-
// attention surface, previously human-only), org_huddle_end (which closes the
|
|
79
|
-
// loop org_huddle_start opened) and memory_recall — hq built memory.recall
|
|
80
|
-
// explicitly because "an SDK-driven seat has been reasoning off a strictly
|
|
81
|
-
// smaller memory than the chat lane", then shipped no tool for it, so the
|
|
82
|
-
// reverse-parity fix never reached the plane it was built for.
|
|
83
|
-
// Curated 35 → 38.
|
|
84
|
-
// The ENGAGEMENT delta adds the four verbs an SDK seat needs to bring somebody
|
|
85
|
-
// in and had no binding for, despite all four sitting in the frozen protocol
|
|
86
|
-
// table since it was written: messaging_open_group (channel.createConversation),
|
|
87
|
-
// messaging_create_channel (channel.create), messaging_add_to_channel
|
|
88
|
-
// (channel.addMember), and engage_colleagues — the judged path that decides
|
|
89
|
-
// WHETHER anyone needs involving before it opens anything at all.
|
|
90
|
-
// Curated 38 → 42, +1 for work_track → 43.
|
|
91
|
-
// The VOICE delta adds messaging_send_voice_note — hq could synthesise a
|
|
92
|
-
// colleague's voice note only as a reflex (answering a human's spoken note in
|
|
93
|
-
// kind), so an agent ASKED in text for one had no verb and truthfully said it
|
|
94
|
-
// could not. A `local` binding: it records (messaging.synthesizeVoiceNote)
|
|
95
|
-
// then posts (plain messaging.send with the file attached). Curated 43 → 44.
|
|
96
|
-
// The MANDATE + REGISTRY + PREFERENCE delta (+10 → 54). Three families that
|
|
97
|
-
// were protocol-declared and curated NOWHERE, so a session could reach them
|
|
98
|
-
// only through `org_rpc` — access:"admin", i.e. not at all below the CEO tier:
|
|
99
|
-
// mandate (5) — hq's own chat colleague has carried the whole belt
|
|
100
|
-
// since the spine landed, under a docblock naming SDK
|
|
101
|
-
// parity as its reason; the SDK plane had none of it, so
|
|
102
|
-
// the daemon knew the seat's objectives and the session it
|
|
103
|
-
// spawned did not. adopt/retire are deliberately absent —
|
|
104
|
-
// `mandate.write` is not in DEFAULT_AGENT_SCOPES.
|
|
105
|
-
// subagent (3) — READS only. hq keeps the registry, maestro generates
|
|
106
|
-
// sub-agents locally, and the two halves never met. The
|
|
107
|
-
// writes stay with lib/subagents/client.mjs, which owns
|
|
108
|
-
// the offline outbox and the lock file a second writer
|
|
109
|
-
// would silently bypass.
|
|
110
|
-
// preference (2) — hq built this family FOR this plane and said so in the
|
|
111
|
-
// handler; no tool was ever added, so the same workspace
|
|
112
|
-
// remembered a standing preference when hq answered and
|
|
113
|
-
// forgot it when the seat's own daemon did.
|
|
114
|
-
// The RESOURCE delta (+8 desk → 77, table 133 → 141). A venture's artifacts
|
|
115
|
-
// cross from the portal and are filed as real WorkspaceFile rows, and the
|
|
116
|
-
// OrgResource catalogue that indexes them was reachable from NO agent code
|
|
117
|
-
// path in EITHER plane — the venture's own colleagues could not list, add,
|
|
118
|
-
// correct, reorder or remove one of their own deliverables. Unlike every
|
|
119
|
-
// block above it, this one is GENERATED from the vendored protocol
|
|
120
|
-
// declaration (lib/org/resource-tools.mjs) rather than transcribed from hq's
|
|
121
|
-
// desk, so it cannot acquire the 239-vs-69 drift the hand-copied desks
|
|
122
|
-
// already carry; resource-tools.test.mjs is the parity that holds it there.
|
|
123
|
-
// The FRONT-DOOR delta (+3 curated → 57, table 141 → 144; design spec
|
|
124
|
-
// 2026-09-08 §3.5/§3.7): board_mine (the agent's own items across every
|
|
125
|
-
// board — board.ready is unassigned-only, so "my tasks" was unreadable from
|
|
126
|
-
// any plane), board_track (the sanctioned inbound→board seam with the
|
|
127
|
-
// accepted/done ends of the ladder and title/why passthrough) and
|
|
128
|
-
// session_status (the main session's liveness + peers from local state,
|
|
129
|
-
// offline-capable).
|
|
130
|
-
assert.equal(ORG_TOOLS.length, 144, "57 curated + 5 email + 5 artifact + 77 desk");
|
|
131
|
-
assert.equal(ORG_TOOLS.filter((t) => t.desk).length, 77, "exactly seventy-seven desk tools");
|
|
132
|
-
assert.equal(ORG_TOOLS.filter((t) => t.email).length, 5, "exactly five email tools");
|
|
133
|
-
assert.equal(ORG_TOOLS.filter((t) => t.artifact).length, 5, "exactly five artifact tools");
|
|
134
|
-
for (const t of ORG_TOOLS) {
|
|
135
|
-
assert.match(t.name, /^[a-z0-9_]+$/, `${t.name} snake_case`);
|
|
136
|
-
assert.ok(["read", "write", "admin"].includes(t.access), `${t.name} access`);
|
|
137
|
-
assert.ok(t.description && t.description.length > 10, `${t.name} described`);
|
|
138
|
-
assert.equal(t.input_schema.type, "object", `${t.name} schema object`);
|
|
139
|
-
assert.ok(t.binding && ["rpc", "read", "local"].includes(t.binding.kind), `${t.name} binding`);
|
|
140
|
-
if (t.binding.kind === "rpc") {
|
|
141
|
-
assert.ok(methodDef(t.binding.method), `${t.name} binds a real protocol method (${t.binding.method})`);
|
|
142
|
-
}
|
|
143
|
-
// The rpc side was already pinned to the frozen table; the READ side was
|
|
144
|
-
// not, so a curated tool could name a read path the server does not serve
|
|
145
|
-
// and only fail at runtime, as a bare 404 the model cannot interpret.
|
|
146
|
-
if (t.binding.kind === "read") {
|
|
147
|
-
assert.ok(
|
|
148
|
-
Object.prototype.hasOwnProperty.call(READS, t.binding.path),
|
|
149
|
-
`${t.name} binds a real protocol read (${t.binding.path})`,
|
|
150
|
-
);
|
|
151
|
-
if (t.binding.query !== undefined) {
|
|
152
|
-
assert.equal(typeof t.binding.query, "object", `${t.name} binding.query is an object`);
|
|
153
|
-
for (const [k, v] of Object.entries(t.binding.query)) {
|
|
154
|
-
assert.equal(typeof v, "string", `${t.name} binding.query.${k} is a string`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("OUTBOUND_METHODS is DERIVED from outbound:true tools (messaging/email/mail-desk/share-step sends)", () => {
|
|
162
|
-
assert.deepEqual([...OUTBOUND_METHODS].sort(), ["calling.appShareAct", "email.draftSend", "email.send", "messaging.send"]);
|
|
163
|
-
// messaging_send_voice_note is outbound-flagged but adds NOTHING to
|
|
164
|
-
// OUTBOUND_METHODS: it is a `local` binding, and the derivation reads rpc
|
|
165
|
-
// bindings only. Its POST leg is messaging.send, which is already in the set,
|
|
166
|
-
// so the escape hatch stays covered too — the flag here buys the screen on
|
|
167
|
-
// the SPOKEN words, before a billable character is synthesised.
|
|
168
|
-
const outboundTools = ORG_TOOLS.filter((t) => t.outbound).map((t) => t.name).sort();
|
|
169
|
-
assert.deepEqual(outboundTools, [
|
|
170
|
-
"email_draft_send",
|
|
171
|
-
"email_send",
|
|
172
|
-
"messaging_send",
|
|
173
|
-
"messaging_send_voice_note",
|
|
174
|
-
"org_call_share_step",
|
|
175
|
-
]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("email gating: family present in the vendored protocol → tools active; override excludes", () => {
|
|
179
|
-
// The email family landed in the vendored protocol (sync-protocol phase 1).
|
|
180
|
-
assert.equal(emailFamilyAvailable(), true, "vendored protocol carries email.send");
|
|
181
|
-
assert.equal(getOrgTools().length, 144);
|
|
182
|
-
const without = getOrgTools({ emailAvailable: false });
|
|
183
|
-
assert.equal(without.length, 139);
|
|
184
|
-
assert.ok(!without.some((t) => t.email), "email tools excluded when family absent");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("artifact gating: family present → tools active; override excludes (email precedent)", () => {
|
|
188
|
-
assert.equal(artifactFamilyAvailable(), true, "vendored protocol carries artifact.act");
|
|
189
|
-
const without = getOrgTools({ artifactAvailable: false });
|
|
190
|
-
assert.equal(without.length, 139);
|
|
191
|
-
assert.ok(!without.some((t) => t.artifact), "artifact tools excluded when family absent");
|
|
192
|
-
const neither = getOrgTools({ emailAvailable: false, artifactAvailable: false, desksAvailable: false });
|
|
193
|
-
assert.equal(neither.length, 57, "all additive families off → the 57 always-on tools");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("isOrgTool / orgToolDef cover the full table; unknown names rejected", () => {
|
|
197
|
-
for (const t of ORG_TOOLS) assert.equal(isOrgTool(t.name), true);
|
|
198
|
-
assert.equal(isOrgTool("slack_send"), false);
|
|
199
|
-
assert.equal(isOrgTool(""), false);
|
|
200
|
-
assert.equal(orgToolDef("org_rpc").access, "admin");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// ── config resolution (§1.4) ───────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
test("resolveOrgToolConfig: config base/token honoured; default base when nothing names one", () => {
|
|
206
|
-
const cfg = resolveOrgToolConfig({ orgConfig: CFG, agentRoot: "/tmp/none" });
|
|
207
|
-
assert.equal(cfg.base, "https://org.example");
|
|
208
|
-
assert.equal(cfg.token, "nlk_secret");
|
|
209
|
-
assert.equal(cfg.orgId, "acme");
|
|
210
|
-
const bare = resolveOrgToolConfig({ orgConfig: {}, agentRoot: "/tmp/none" });
|
|
211
|
-
assert.equal(bare.base, DEFAULT_COHORT_BASE);
|
|
212
|
-
assert.equal(bare.token, "");
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("resolveOrgToolConfig: COHORT_BASE env wins over config base (env-first)", () => {
|
|
216
|
-
process.env.COHORT_BASE = "https://env.example/";
|
|
217
|
-
try {
|
|
218
|
-
const cfg = resolveOrgToolConfig({ orgConfig: CFG, agentRoot: "/tmp/none" });
|
|
219
|
-
assert.equal(cfg.base, "https://env.example", "env base wins, trailing slash stripped");
|
|
220
|
-
} finally {
|
|
221
|
-
delete process.env.COHORT_BASE;
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// ── executor: basics ───────────────────────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
test("executeOrgTool: unknown tool → NOT_FOUND frame, never a throw", async () => {
|
|
228
|
-
const frame = await executeOrgTool("nope_tool", {}, { orgConfig: CFG, agentRoot: "/tmp/none" });
|
|
229
|
-
assert.equal(frame.ok, false);
|
|
230
|
-
assert.equal(frame.error.code, "NOT_FOUND");
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("executeOrgTool: missing token → clear UNAUTHORIZED frame for network tools", async () => {
|
|
234
|
-
const fetchImpl = fakeFetch();
|
|
235
|
-
const frame = await executeOrgTool("org_directory", {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
|
|
236
|
-
assert.equal(frame.ok, false);
|
|
237
|
-
assert.equal(frame.error.code, "UNAUTHORIZED");
|
|
238
|
-
assert.match(frame.error.message, /COHORT_API_TOKEN/);
|
|
239
|
-
assert.equal(fetchImpl.calls.length, 0, "no network attempted");
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("org_describe: offline, no network, family filter works", async () => {
|
|
243
|
-
const fetchImpl = fakeFetch();
|
|
244
|
-
const frame = await executeOrgTool("org_describe", {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
|
|
245
|
-
assert.equal(frame.ok, true);
|
|
246
|
-
assert.equal(frame.result.protocolVersion, PROTOCOL_VERSION);
|
|
247
|
-
assert.ok(frame.result.methods.length >= 200, "full method table listed");
|
|
248
|
-
assert.ok(frame.result.reads.length >= 13, "reads listed");
|
|
249
|
-
const fam = await executeOrgTool("org_describe", { family: "board" }, { orgConfig: {}, agentRoot: "/tmp/none" });
|
|
250
|
-
assert.ok(fam.result.methods.every((m) => m.family === "board"));
|
|
251
|
-
assert.deepEqual(fam.result.families, ["board"]);
|
|
252
|
-
assert.equal(fetchImpl.calls.length, 0);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// ── executor: rpc + read bindings ──────────────────────────────────────────
|
|
256
|
-
|
|
257
|
-
test("rpc binding: messaging_history POSTs /api/v1/messaging.history with Bearer + org pin", async () => {
|
|
258
|
-
const fetchImpl = fakeFetch({ ok: true, result: { messages: [] } });
|
|
259
|
-
const frame = await executeOrgTool("messaging_history", { channelId: "C1", limit: 5 }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
260
|
-
assert.equal(frame.ok, true);
|
|
261
|
-
assert.equal(fetchImpl.calls.length, 1);
|
|
262
|
-
const { url, init } = fetchImpl.calls[0];
|
|
263
|
-
assert.equal(url, "https://org.example/api/v1/messaging.history");
|
|
264
|
-
assert.equal(init.method, "POST");
|
|
265
|
-
assert.equal(init.headers.authorization, "Bearer nlk_secret");
|
|
266
|
-
assert.equal(init.headers["x-org-id"], "acme");
|
|
267
|
-
assert.deepEqual(JSON.parse(init.body), { channelId: "C1", limit: 5 });
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test("read binding: board_ready GETs /api/v1/board.ready and normalises the bare payload", async () => {
|
|
271
|
-
const fetchImpl = fakeFetch({ items: [{ id: "w1" }] });
|
|
272
|
-
const frame = await executeOrgTool("board_ready", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
273
|
-
assert.equal(frame.ok, true);
|
|
274
|
-
assert.deepEqual(frame.result, { items: [{ id: "w1" }] });
|
|
275
|
-
assert.equal(fetchImpl.calls[0].init.method, "GET");
|
|
276
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/board\.ready$/);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// ── macro pulse (the founder's attention surface, previously human-only) ────
|
|
280
|
-
|
|
281
|
-
test("org_pulse: the binding PINS ?pulse=1 — the agent never has to know the param", async () => {
|
|
282
|
-
const fetchImpl = fakeFetch({
|
|
283
|
-
chainHead: { seq: 42, rowHash: "abc" },
|
|
284
|
-
members: 9,
|
|
285
|
-
openAlerts: 2,
|
|
286
|
-
readyTasks: 5,
|
|
287
|
-
pulse: {
|
|
288
|
-
stats: { agentsRunning: 3, blocked: 1, decisionsToday: 2, needs: 2 },
|
|
289
|
-
needs: [{ id: "esc_1", severity: "high", what: "Blocked on pricing" }],
|
|
290
|
-
activity: [{ id: "ev_0", sentence: "Assigned a card in Growth" }],
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
const frame = await executeOrgTool("org_pulse", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
294
|
-
assert.equal(frame.ok, true);
|
|
295
|
-
assert.equal(fetchImpl.calls[0].init.method, "GET");
|
|
296
|
-
// Without a pinned query this GETs the bare probe and the pulse is null — the
|
|
297
|
-
// whole capability turns on this one param riding the binding.
|
|
298
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/ops\?pulse=1$/);
|
|
299
|
-
assert.equal(frame.result.pulse.stats.blocked, 1);
|
|
300
|
-
assert.equal(frame.result.pulse.needs[0].severity, "high");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test("org_pulse is reachable at the READ tier — not another admin-only escape hatch", () => {
|
|
304
|
-
const pulse = orgToolDef("org_pulse");
|
|
305
|
-
assert.equal(pulse.access, "read", "an ordinary agent must be able to ask what needs attention");
|
|
306
|
-
assert.equal(pulse.desk, undefined, "always-on: `ops` has been in READS since the first protocol");
|
|
307
|
-
// The failure mode the description must pre-empt: reporting "nothing needs
|
|
308
|
-
// attention" when the derivation actually failed.
|
|
309
|
-
assert.match(pulse.description, /pulse: null/);
|
|
310
|
-
assert.match(pulse.description, /do not report zero/);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test("huddle lifecycle is CLOSED: a room an agent can open is a room it can enter and shut", async () => {
|
|
314
|
-
const names = ORG_TOOLS.filter((t) => t.name.startsWith("org_huddle_")).map((t) => t.name).sort();
|
|
315
|
-
assert.deepEqual(names, ["org_huddle_end", "org_huddle_join", "org_huddle_start"]);
|
|
316
|
-
for (const n of names) {
|
|
317
|
-
const def = orgToolDef(n);
|
|
318
|
-
assert.equal(def.desk, undefined, `${n} is always-on (the base calling family predates the desks)`);
|
|
319
|
-
assert.equal(def.access, "write");
|
|
320
|
-
assert.ok(def.binding.method.startsWith("calling."), `${n} rides the calling family`);
|
|
321
|
-
}
|
|
322
|
-
const fetchImpl = fakeFetch({ ok: true, result: { callId: "call_1", endedAt: "2026-08-12T10:00:00.000Z" } });
|
|
323
|
-
const frame = await executeOrgTool("org_huddle_end", { callId: "call_1" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
324
|
-
assert.equal(frame.ok, true);
|
|
325
|
-
assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/calling.end");
|
|
326
|
-
assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body), { callId: "call_1" });
|
|
327
|
-
// CONFLICT means "already closed", not "you failed" — the description must
|
|
328
|
-
// say so, or a model retries a call it successfully ended.
|
|
329
|
-
assert.match(orgToolDef("org_huddle_end").description, /CONFLICT/);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test("org_directory advertises the derived presence lane (a field nobody is told about is unreachable)", () => {
|
|
333
|
-
const dir = orgToolDef("org_directory");
|
|
334
|
-
for (const needle of ["presence", "lane", "detail", "indisposed"]) {
|
|
335
|
-
assert.match(dir.description, new RegExp(needle), `org_directory names ${needle}`);
|
|
336
|
-
}
|
|
337
|
-
// The two honest-reading rules: a human seat's null is not "offline", and the
|
|
338
|
-
// raw client-stamped `status.ts` is not the dot.
|
|
339
|
-
assert.match(dir.description, /presence: null/);
|
|
340
|
-
assert.match(dir.description, /client-stamped/);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
test("memory_recall: the SDK seat gets the same memory the chat lane reasons off", async () => {
|
|
344
|
-
const recall = orgToolDef("memory_recall");
|
|
345
|
-
assert.equal(recall.access, "read", "recall is read-only — it must not need a write tier");
|
|
346
|
-
assert.equal(recall.binding.method, "memory.recall");
|
|
347
|
-
// knowledge.search is a NARROWED PROJECTION of the same index; if the model is
|
|
348
|
-
// not told that, it keeps reaching for the smaller one out of habit.
|
|
349
|
-
assert.match(orgToolDef("knowledge_search").description, /memory_recall/);
|
|
350
|
-
// The two contracts a model gets wrong without being told: the entity pin is
|
|
351
|
-
// a PAIR, and an empty index is a fact, not a failure to retry.
|
|
352
|
-
assert.match(recall.description, /TOGETHER/);
|
|
353
|
-
assert.match(recall.description, /found:false/);
|
|
354
|
-
|
|
355
|
-
const fetchImpl = fakeFetch({ ok: true, result: { found: true, items: [{ id: "m1" }] } });
|
|
356
|
-
const frame = await executeOrgTool(
|
|
357
|
-
"memory_recall",
|
|
358
|
-
{ query: "what did we decide about pricing", scope: "self", depth: "specific", k: 5 },
|
|
359
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
360
|
-
);
|
|
361
|
-
assert.equal(frame.ok, true);
|
|
362
|
-
assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/memory.recall");
|
|
363
|
-
assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body), {
|
|
364
|
-
query: "what did we decide about pricing", scope: "self", depth: "specific", k: 5,
|
|
365
|
-
});
|
|
366
|
-
// A read carries no dispatcher idempotency key.
|
|
367
|
-
assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test("email_triage exposes every facet the server accepts (mark-unread-from, muted, done)", () => {
|
|
371
|
-
const triage = orgToolDef("email_triage");
|
|
372
|
-
const props = Object.keys(triage.input_schema.properties).sort();
|
|
373
|
-
assert.deepEqual(props, [
|
|
374
|
-
"assigneeMemberId", "category", "done", "muted", "priority",
|
|
375
|
-
"read", "starred", "tags", "threadId", "unreadFromMessageId",
|
|
376
|
-
], "the schema matches email.triage's real param set — an undocumented param is an unreachable one");
|
|
377
|
-
// The exclusivity rules are the server's; a model that does not know them
|
|
378
|
-
// burns a turn on a BAD_REQUEST it cannot diagnose.
|
|
379
|
-
assert.match(triage.description, /mutually exclusive/);
|
|
380
|
-
assert.match(triage.description, /one-verb-per-call/);
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test("org_events_tail: cursor + clamped limit ride the query string", async () => {
|
|
384
|
-
const fetchImpl = fakeFetch({ events: [] });
|
|
385
|
-
await executeOrgTool("org_events_tail", { cursor: 7, limit: 9999 }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
386
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/events\?cursor=7&limit=500$/, "limit clamped to 500");
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
test("approval_wait: requires approvalId; dispatches the bounded long-poll", async () => {
|
|
390
|
-
const missing = await executeOrgTool("approval_wait", {}, { orgConfig: CFG, agentRoot: "/tmp/none" });
|
|
391
|
-
assert.equal(missing.error.code, "BAD_REQUEST");
|
|
392
|
-
const fetchImpl = fakeFetch({ id: "a1", status: "approved" });
|
|
393
|
-
const frame = await executeOrgTool("approval_wait", { approvalId: "a1" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
394
|
-
assert.equal(frame.ok, true);
|
|
395
|
-
assert.match(fetchImpl.calls[0].url, /approval\.wait\?id=a1&timeoutMs=55000/);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// ── executor: send-gate parity (§1.5a) ─────────────────────────────────────
|
|
399
|
-
|
|
400
|
-
test("messaging_send: screened through the send-gate, idempotencyId auto-minted", async () => {
|
|
401
|
-
const screened = [];
|
|
402
|
-
const fetchImpl = fakeFetch({ ok: true, result: { messageId: "m1" } });
|
|
403
|
-
const frame = await executeOrgTool(
|
|
404
|
-
"messaging_send",
|
|
405
|
-
{ channelId: "C9", body: "hello org" },
|
|
406
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
|
|
407
|
-
);
|
|
408
|
-
assert.equal(frame.ok, true);
|
|
409
|
-
assert.equal(screened.length, 1, "screenOutbound ran");
|
|
410
|
-
assert.equal(screened[0].recipient, "C9");
|
|
411
|
-
assert.equal(screened[0].text, "hello org");
|
|
412
|
-
const body = JSON.parse(fetchImpl.calls[0].init.body);
|
|
413
|
-
// hq's sendMessageSchemaV1 REQUIRES `idempotencyId`. Minting `clientMsgId`
|
|
414
|
-
// (the column name, not the wire name) 400'd every message this plane sent.
|
|
415
|
-
assert.ok(body.idempotencyId && body.idempotencyId.length >= 8, "idempotencyId auto-minted");
|
|
416
|
-
assert.equal(body.clientMsgId, undefined, "the column name is NOT the wire name");
|
|
417
|
-
assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "dispatcher idempotency key present");
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
test("messaging_send: a vetoing gate blocks BEFORE any network", async () => {
|
|
421
|
-
const fetchImpl = fakeFetch();
|
|
422
|
-
const frame = await executeOrgTool(
|
|
423
|
-
"messaging_send",
|
|
424
|
-
{ channelId: "C9", body: "As an AI I love this" },
|
|
425
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: false, reason: "banned-phrase" }) },
|
|
426
|
-
);
|
|
427
|
-
assert.equal(frame.ok, false);
|
|
428
|
-
assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
|
|
429
|
-
assert.match(frame.error.message, /banned-phrase/);
|
|
430
|
-
assert.equal(fetchImpl.calls.length, 0, "blocked before dispatch");
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
test("a THROWING gate fails open with a counted diagnostic (adapter parity)", async () => {
|
|
434
|
-
const counted = [];
|
|
435
|
-
const fetchImpl = fakeFetch({ ok: true, result: {} });
|
|
436
|
-
const frame = await executeOrgTool(
|
|
437
|
-
"messaging_send",
|
|
438
|
-
{ channelId: "C9", body: "hi" },
|
|
439
|
-
{
|
|
440
|
-
orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl,
|
|
441
|
-
screenImpl: async () => { throw new Error("gate exploded"); },
|
|
442
|
-
bumpImpl: (name, attrs) => counted.push({ name, attrs }),
|
|
443
|
-
},
|
|
444
|
-
);
|
|
445
|
-
assert.equal(frame.ok, true, "fail-open");
|
|
446
|
-
assert.equal(counted[0].name, "channel.send_gate.fail_open");
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// ── executor: escape hatches ───────────────────────────────────────────────
|
|
450
|
-
|
|
451
|
-
test("org_rpc: unknown method rejected against the protocol table, NO network", async () => {
|
|
452
|
-
const fetchImpl = fakeFetch();
|
|
453
|
-
const frame = await executeOrgTool("org_rpc", { method: "nope.nothing" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
454
|
-
assert.equal(frame.ok, false);
|
|
455
|
-
assert.equal(frame.error.code, "NOT_FOUND");
|
|
456
|
-
assert.equal(fetchImpl.calls.length, 0, "validated before any network");
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
test("org_rpc: escape-hatch parity — messaging.send through org_rpc is ALSO screened", async () => {
|
|
460
|
-
const screened = [];
|
|
461
|
-
const fetchImpl = fakeFetch({ ok: true, result: {} });
|
|
462
|
-
await executeOrgTool(
|
|
463
|
-
"org_rpc",
|
|
464
|
-
{ method: "messaging.send", params: { channelId: "C2", body: "raw lane" } },
|
|
465
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
|
|
466
|
-
);
|
|
467
|
-
assert.equal(screened.length, 1, "escape hatch screened");
|
|
468
|
-
assert.match(screened[0].text, /raw lane/, "params blob screened");
|
|
469
|
-
// and a blocked verdict stops the dispatch
|
|
470
|
-
const fetch2 = fakeFetch();
|
|
471
|
-
const blocked = await executeOrgTool(
|
|
472
|
-
"org_rpc",
|
|
473
|
-
{ method: "email.send", params: { to: ["x@y.z"], text: "leak" } },
|
|
474
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: fetch2, screenImpl: async () => ({ allow: false, reason: "no" }) },
|
|
475
|
-
);
|
|
476
|
-
assert.equal(blocked.ok, false);
|
|
477
|
-
assert.equal(fetch2.calls.length, 0);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
test("org_rpc: non-outbound method dispatches unscreened with optional idempotencyKey", async () => {
|
|
481
|
-
const screened = [];
|
|
482
|
-
const fetchImpl = fakeFetch({ ok: true, result: { got: 1 } });
|
|
483
|
-
const frame = await executeOrgTool(
|
|
484
|
-
"org_rpc",
|
|
485
|
-
{ method: "member.get", params: { memberId: "m-1" }, idempotencyKey: "k-1" },
|
|
486
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
|
|
487
|
-
);
|
|
488
|
-
assert.equal(frame.ok, true);
|
|
489
|
-
assert.equal(screened.length, 0, "reads/non-outbound never screened");
|
|
490
|
-
assert.match(fetchImpl.calls[0].url, /member\.get$/);
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
test("org_read: validated against READS; query params encoded; unknown path rejected", async () => {
|
|
494
|
-
const fetchImpl = fakeFetch({ events: [] });
|
|
495
|
-
const frame = await executeOrgTool("org_read", { path: "events", query: { cursor: 3, limit: 10 } }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
496
|
-
assert.equal(frame.ok, true);
|
|
497
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/events\?cursor=3&limit=10$/);
|
|
498
|
-
const bad = await executeOrgTool("org_read", { path: "not-a-read" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
499
|
-
assert.equal(bad.error.code, "NOT_FOUND");
|
|
500
|
-
assert.equal(fetchImpl.calls.length, 1, "unknown path never dispatched");
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
// ── executor: email tools ──────────────────────────────────────────────────
|
|
504
|
-
|
|
505
|
-
test("email_send: idempotencyId auto-minted; screened; rides email.send", async () => {
|
|
506
|
-
const screened = [];
|
|
507
|
-
const fetchImpl = fakeFetch({ ok: true, result: { status: "queued", messageId: "e1" } });
|
|
508
|
-
const frame = await executeOrgTool(
|
|
509
|
-
"email_send",
|
|
510
|
-
{ to: ["a@ext.com"], subject: "Hi", text: "Body" },
|
|
511
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
|
|
512
|
-
);
|
|
513
|
-
assert.equal(frame.ok, true);
|
|
514
|
-
assert.equal(screened[0].recipient, "a@ext.com");
|
|
515
|
-
assert.match(screened[0].text, /Subject: Hi/);
|
|
516
|
-
const body = JSON.parse(fetchImpl.calls[0].init.body);
|
|
517
|
-
assert.ok(body.idempotencyId && body.idempotencyId.length >= 8, "idempotencyId auto-minted");
|
|
518
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/email\.send$/);
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
test("email tools degrade to NOT_FOUND when the family is gated off (pre-sync install)", async () => {
|
|
522
|
-
const fetchImpl = fakeFetch();
|
|
523
|
-
const frame = await executeOrgTool("email_inbox", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, emailAvailable: false });
|
|
524
|
-
assert.equal(frame.ok, false);
|
|
525
|
-
assert.equal(frame.error.code, "NOT_FOUND");
|
|
526
|
-
assert.match(frame.error.message, /upgrade/i);
|
|
527
|
-
assert.equal(fetchImpl.calls.length, 0);
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// ── executor: artifact tools ───────────────────────────────────────────────
|
|
531
|
-
|
|
532
|
-
test("artifact_create: clientMsgId auto-minted; unscreened (in-thread, not outbound); rides artifact.create", async () => {
|
|
533
|
-
const screened = [];
|
|
534
|
-
const envelope = { genui: "v2", artifact: { class: "ops.status", version: 1, state: "complete", source: { kind: "native" } }, actions: [], root: { type: "card", tone: "neutral", blocks: [] } };
|
|
535
|
-
const fetchImpl = fakeFetch({ ok: true, result: { artifactId: "a1", messageId: "m1" } });
|
|
536
|
-
const frame = await executeOrgTool(
|
|
537
|
-
"artifact_create",
|
|
538
|
-
{ channelId: "C1", envelope },
|
|
539
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
|
|
540
|
-
);
|
|
541
|
-
assert.equal(frame.ok, true);
|
|
542
|
-
assert.deepEqual(frame.result, { artifactId: "a1", messageId: "m1" });
|
|
543
|
-
assert.equal(screened.length, 0, "artifact posts are in-thread — never send-gate screened");
|
|
544
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/artifact\.create$/);
|
|
545
|
-
const body = JSON.parse(fetchImpl.calls[0].init.body);
|
|
546
|
-
assert.ok(body.clientMsgId && body.clientMsgId.length >= 8, "clientMsgId auto-minted");
|
|
547
|
-
assert.deepEqual(body.envelope, envelope, "envelope posted verbatim");
|
|
548
|
-
assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined, "sideEffecting:false — no dispatcher key; the service dedupes on clientMsgId");
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test("artifact_act: idempotencyKey auto-minted when absent, caller's key preserved; held result is a NORMAL frame", async () => {
|
|
552
|
-
const fetchImpl = fakeFetch({ ok: true, result: { held: true, approvalId: "ap-1" } });
|
|
553
|
-
const frame = await executeOrgTool(
|
|
554
|
-
"artifact_act",
|
|
555
|
-
{ artifactId: "a1", action: "approve" },
|
|
556
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
557
|
-
);
|
|
558
|
-
assert.equal(frame.ok, true, "approval-held is a result, not an error");
|
|
559
|
-
assert.equal(frame.result.held, true);
|
|
560
|
-
assert.equal(frame.result.approvalId, "ap-1");
|
|
561
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/artifact\.act$/);
|
|
562
|
-
const minted = JSON.parse(fetchImpl.calls[0].init.body);
|
|
563
|
-
assert.ok(minted.idempotencyKey && minted.idempotencyKey.length >= 8, "idempotencyKey auto-minted");
|
|
564
|
-
// A caller-supplied key (retry / post-approval re-invoke) rides through untouched.
|
|
565
|
-
await executeOrgTool(
|
|
566
|
-
"artifact_act",
|
|
567
|
-
{ artifactId: "a1", action: "approve", idempotencyKey: "approve-a1" },
|
|
568
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
569
|
-
);
|
|
570
|
-
assert.equal(JSON.parse(fetchImpl.calls[1].init.body).idempotencyKey, "approve-a1");
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
test("artifact reads ride their rpc methods; tools degrade to NOT_FOUND when the family is gated off", async () => {
|
|
574
|
-
const fetchImpl = fakeFetch({ ok: true, result: { items: [] } });
|
|
575
|
-
await executeOrgTool("artifact_list", { channelId: "C1", state: "complete" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
576
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/artifact\.list$/);
|
|
577
|
-
await executeOrgTool("artifact_catalog", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
578
|
-
assert.match(fetchImpl.calls[1].url, /\/api\/v1\/artifact\.catalog$/);
|
|
579
|
-
const gated = fakeFetch();
|
|
580
|
-
const frame = await executeOrgTool("artifact_get", { artifactId: "a1" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gated, artifactAvailable: false });
|
|
581
|
-
assert.equal(frame.ok, false);
|
|
582
|
-
assert.equal(frame.error.code, "NOT_FOUND");
|
|
583
|
-
assert.match(frame.error.message, /upgrade/i);
|
|
584
|
-
assert.equal(gated.calls.length, 0);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// ── executor: directory + design desks (2026-07) ───────────────────────────
|
|
588
|
-
|
|
589
|
-
test("directory/design desks gate on their OWN probe, independently of the other desks", () => {
|
|
590
|
-
// One DESK_PROBES line lights a desk up on BOTH planes; a desk whose probe
|
|
591
|
-
// method is absent from the vendored protocol must not register.
|
|
592
|
-
assert.equal(deskFamilyAvailable("directory"), true, "vendored protocol carries directory.search");
|
|
593
|
-
assert.equal(deskFamilyAvailable("design"), true, "vendored protocol carries branding.getFoundation");
|
|
594
|
-
assert.equal(deskFamilyAvailable("nope"), false, "unknown desk → default-deny");
|
|
595
|
-
assert.equal(ORG_TOOLS.filter((t) => t.desk === "directory").length, 14);
|
|
596
|
-
assert.equal(ORG_TOOLS.filter((t) => t.desk === "design").length, 8);
|
|
597
|
-
// Neither desk smuggles an outbound lane in: no directory/design tool sends.
|
|
598
|
-
assert.equal(ORG_TOOLS.filter((t) => (t.desk === "directory" || t.desk === "design") && t.outbound).length, 0);
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
test("directory reads/writes ride their rpc methods; a side-effecting write carries a dispatcher key", async () => {
|
|
602
|
-
const fetchImpl = fakeFetch({ ok: true, result: { people: [] } });
|
|
603
|
-
const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
|
|
604
|
-
await executeOrgTool("directory_search", { q: "hartmann" }, o);
|
|
605
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/directory\.search$/);
|
|
606
|
-
assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body), { q: "hartmann" });
|
|
607
|
-
assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined, "sideEffecting:false read sends no key");
|
|
608
|
-
|
|
609
|
-
await executeOrgTool("directory_list_people", { relationship: "client", sort: "touch" }, o);
|
|
610
|
-
assert.match(fetchImpl.calls[1].url, /\/api\/v1\/directory\.listPeople$/);
|
|
611
|
-
|
|
612
|
-
await executeOrgTool(
|
|
613
|
-
"directory_propose_capture",
|
|
614
|
-
{ kind: "PERSON", payload: { person: { displayName: "Dr. Lena Hartmann" } }, source: "MAIL", sourceFingerprint: "sig:abc" },
|
|
615
|
-
o,
|
|
616
|
-
);
|
|
617
|
-
assert.match(fetchImpl.calls[2].url, /\/api\/v1\/directory\.proposeCapture$/);
|
|
618
|
-
assert.ok(
|
|
619
|
-
fetchImpl.calls[2].init.headers["x-idempotency-key"],
|
|
620
|
-
"sideEffecting write gets a fresh dispatcher key",
|
|
621
|
-
);
|
|
622
|
-
assert.equal(JSON.parse(fetchImpl.calls[2].init.body).sourceFingerprint, "sig:abc");
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test("design tools ride branding.*; the costed quote call posts budgetCents:0 verbatim", async () => {
|
|
626
|
-
const fetchImpl = fakeFetch({ ok: true, result: { slots: [], costCents: 0 } });
|
|
627
|
-
const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
|
|
628
|
-
await executeOrgTool("design_foundation", { historyLimit: 5 }, o);
|
|
629
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/branding\.getFoundation$/);
|
|
630
|
-
await executeOrgTool("design_voice", {}, o);
|
|
631
|
-
assert.match(fetchImpl.calls[1].url, /\/api\/v1\/branding\.getVoice$/);
|
|
632
|
-
await executeOrgTool("design_generate_image", { budgetCents: 0 }, o);
|
|
633
|
-
assert.match(fetchImpl.calls[2].url, /\/api\/v1\/branding\.generateImage$/);
|
|
634
|
-
assert.equal(JSON.parse(fetchImpl.calls[2].init.body).budgetCents, 0, "0 is the QUOTE call, not a falsy drop");
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
test("design_rewrite_in_voice produces copy and is NEVER send-gate screened", async () => {
|
|
638
|
-
// It returns a rewrite; it sends nothing. Whatever the caller does with the
|
|
639
|
-
// result is screened at the send verb, not here.
|
|
640
|
-
const screened = [];
|
|
641
|
-
const fetchImpl = fakeFetch({ ok: true, result: { text: "Rewritten.", costCents: 3 } });
|
|
642
|
-
const frame = await executeOrgTool(
|
|
643
|
-
"design_rewrite_in_voice",
|
|
644
|
-
{ text: "we are excited to announce" },
|
|
645
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (x) => { screened.push(x); return { allow: true }; } },
|
|
646
|
-
);
|
|
647
|
-
assert.equal(frame.ok, true);
|
|
648
|
-
assert.equal(screened.length, 0);
|
|
649
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/branding\.rewriteInVoice$/);
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
test("directory/design tools degrade to NOT_FOUND when their desk is gated off (pre-sync install)", async () => {
|
|
653
|
-
for (const name of ["directory_search", "design_foundation"]) {
|
|
654
|
-
const gated = fakeFetch();
|
|
655
|
-
const frame = await executeOrgTool(name, { q: "x" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gated, desksAvailable: false });
|
|
656
|
-
assert.equal(frame.ok, false, `${name} gated`);
|
|
657
|
-
assert.equal(frame.error.code, "NOT_FOUND");
|
|
658
|
-
assert.match(frame.error.message, /upgrade/i);
|
|
659
|
-
assert.equal(gated.calls.length, 0, `${name} never dispatched`);
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
// ── executor: calls desk (call working sessions, 2026-08) ──────────────────
|
|
664
|
-
|
|
665
|
-
test("calls desk gates on calling.appShareStart; five tools, one outbound verb", () => {
|
|
666
|
-
assert.equal(deskFamilyAvailable("calls"), true, "vendored protocol carries calling.appShareStart");
|
|
667
|
-
const calls = ORG_TOOLS.filter((t) => t.desk === "calls");
|
|
668
|
-
assert.equal(calls.length, 5);
|
|
669
|
-
// The share-step verb is the block's ONE outbound lane: its highlight notes
|
|
670
|
-
// and typed text are read by every human on the call.
|
|
671
|
-
assert.deepEqual(calls.filter((t) => t.outbound).map((t) => t.name), ["org_call_share_step"]);
|
|
672
|
-
assert.deepEqual(
|
|
673
|
-
calls.filter((t) => t.access === "read").map((t) => t.name).sort(),
|
|
674
|
-
["org_call_events", "org_call_visual_context"],
|
|
675
|
-
);
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
test("org_call_share_start/end ride their rpc methods with a fresh dispatcher key", async () => {
|
|
679
|
-
const fetchImpl = fakeFetch({ ok: true, result: { sessionId: "s1" } });
|
|
680
|
-
const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
|
|
681
|
-
const frame = await executeOrgTool(
|
|
682
|
-
"org_call_share_start",
|
|
683
|
-
{ callId: "call-1", surface: { kind: "doc", ref: "f-1", title: "Q3 plan" } },
|
|
684
|
-
o,
|
|
685
|
-
);
|
|
686
|
-
assert.equal(frame.ok, true);
|
|
687
|
-
assert.equal(frame.result.sessionId, "s1");
|
|
688
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/calling\.appShareStart$/);
|
|
689
|
-
assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "sideEffecting write gets a fresh dispatcher key");
|
|
690
|
-
assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body).surface, { kind: "doc", ref: "f-1", title: "Q3 plan" });
|
|
691
|
-
|
|
692
|
-
await executeOrgTool("org_call_share_end", { callId: "call-1", sessionId: "s1" }, o);
|
|
693
|
-
assert.match(fetchImpl.calls[1].url, /\/api\/v1\/calling\.appShareEnd$/);
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
test("org_call_share_step: screened through the send-gate (notes + typed text), then rides calling.appShareAct", async () => {
|
|
697
|
-
const screened = [];
|
|
698
|
-
const fetchImpl = fakeFetch({ ok: true, result: { seq: 4 } });
|
|
699
|
-
const steps = [
|
|
700
|
-
{ k: "scroll", anchor: "block:3" },
|
|
701
|
-
{ k: "highlight", anchor: "block:3", note: "revenue line" },
|
|
702
|
-
{ k: "type", anchor: "block:4", text: "Draft summary" },
|
|
703
|
-
];
|
|
704
|
-
const frame = await executeOrgTool(
|
|
705
|
-
"org_call_share_step",
|
|
706
|
-
{ callId: "call-1", sessionId: "s1", steps },
|
|
707
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (x) => { screened.push(x); return { allow: true }; } },
|
|
708
|
-
);
|
|
709
|
-
assert.equal(frame.ok, true);
|
|
710
|
-
assert.equal(screened.length, 1, "screenOutbound ran");
|
|
711
|
-
assert.equal(screened[0].recipient, "call-1");
|
|
712
|
-
assert.match(screened[0].text, /revenue line/);
|
|
713
|
-
assert.match(screened[0].text, /Draft summary/);
|
|
714
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/calling\.appShareAct$/);
|
|
715
|
-
assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body).steps, steps);
|
|
716
|
-
|
|
717
|
-
// A vetoing gate blocks BEFORE any network — messaging_send parity.
|
|
718
|
-
const fetch2 = fakeFetch();
|
|
719
|
-
const blocked = await executeOrgTool(
|
|
720
|
-
"org_call_share_step",
|
|
721
|
-
{ callId: "call-1", sessionId: "s1", steps: [{ k: "highlight", anchor: "block:1", note: "As an AI" }] },
|
|
722
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: fetch2, screenImpl: async () => ({ allow: false, reason: "banned-phrase" }) },
|
|
723
|
-
);
|
|
724
|
-
assert.equal(blocked.ok, false);
|
|
725
|
-
assert.equal(blocked.error.code, "FORBIDDEN_SCOPE");
|
|
726
|
-
assert.equal(fetch2.calls.length, 0, "blocked before dispatch");
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
test("calls reads ride their rpc methods without a dispatcher key", async () => {
|
|
730
|
-
const fetchImpl = fakeFetch({ ok: true, result: { events: [] } });
|
|
731
|
-
const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
|
|
732
|
-
await executeOrgTool("org_call_visual_context", { callId: "call-1", limit: 10 }, o);
|
|
733
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/calling\.getVisualContext$/);
|
|
734
|
-
assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined, "sideEffecting:false read sends no key");
|
|
735
|
-
await executeOrgTool("org_call_events", { callId: "call-1" }, o);
|
|
736
|
-
assert.match(fetchImpl.calls[1].url, /\/api\/v1\/calling\.getCallEvents$/);
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
test("calls tools degrade to NOT_FOUND when the desk is gated off (pre-sync install)", async () => {
|
|
740
|
-
const gated = fakeFetch();
|
|
741
|
-
const frame = await executeOrgTool(
|
|
742
|
-
"org_call_share_start",
|
|
743
|
-
{ callId: "call-1", surface: { kind: "doc", ref: "f-1" } },
|
|
744
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gated, desksAvailable: false },
|
|
745
|
-
);
|
|
746
|
-
assert.equal(frame.ok, false);
|
|
747
|
-
assert.equal(frame.error.code, "NOT_FOUND");
|
|
748
|
-
assert.match(frame.error.message, /upgrade/i);
|
|
749
|
-
assert.equal(gated.calls.length, 0, "never dispatched");
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
// ── executor: org_whoami ───────────────────────────────────────────────────
|
|
753
|
-
|
|
754
|
-
test("org_whoami: unenrolled → config summary with member:null + note, never an error", async () => {
|
|
755
|
-
const frame = await executeOrgTool("org_whoami", {}, { orgConfig: {}, agentRoot: "/tmp/none" });
|
|
756
|
-
assert.equal(frame.ok, true);
|
|
757
|
-
assert.equal(frame.result.member, null);
|
|
758
|
-
assert.equal(frame.result.tokenPresent, false);
|
|
759
|
-
assert.match(frame.result.note, /setup --only org|COHORT_API_TOKEN|COHORT_ORG_ID/);
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
test("org_whoami: resolves the member via the org-data whoami lane; token never in the result", async () => {
|
|
763
|
-
const fetchImpl = fakeFetch({ slug: "astra", displayName: "Astra", agentEmail: "astra@agents.example" });
|
|
764
|
-
const frame = await executeOrgTool("org_whoami", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
765
|
-
assert.equal(frame.ok, true);
|
|
766
|
-
assert.equal(frame.result.member.slug, "astra");
|
|
767
|
-
assert.equal(frame.result.tokenPresent, true);
|
|
768
|
-
assert.equal(frame.result.protocolVersion, PROTOCOL_VERSION);
|
|
769
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/org\/whoami\?orgId=acme/);
|
|
770
|
-
assert.ok(!JSON.stringify(frame).includes("nlk_secret"), "token never surfaces");
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
test("org_whoami: unresolvable member degrades to summary + hint note (no error frame)", async () => {
|
|
774
|
-
const fetchImpl = fakeFetch({ error: { code: "NOT_FOUND", message: "ambiguous" } }, { status: 404, ok: false });
|
|
775
|
-
const frame = await executeOrgTool("org_whoami", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
776
|
-
assert.equal(frame.ok, true);
|
|
777
|
-
assert.equal(frame.result.member, null);
|
|
778
|
-
assert.match(frame.result.note, /COHORT_AGENT_ID/);
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
test("org_whoami: COHORT_AGENT_ID hint routes to the direct member GET", async () => {
|
|
782
|
-
const fetchImpl = fakeFetch({ slug: "astra", displayName: "Astra" });
|
|
783
|
-
const frame = await executeOrgTool("org_whoami", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, env: { COHORT_AGENT_ID: "astra" } });
|
|
784
|
-
assert.equal(frame.ok, true);
|
|
785
|
-
assert.match(fetchImpl.calls[0].url, /\/api\/v1\/org\/members\/astra\?orgId=acme/);
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
// ── engage_colleagues: the judged path through the tool plane ──────────────
|
|
789
|
-
|
|
790
|
-
test("engage_colleagues returns an OK frame when it decides NOBODY needs involving", async () => {
|
|
791
|
-
// "Nobody needed involving" is an answer. Returning it as an error would teach
|
|
792
|
-
// the model to retry until it manages to interrupt somebody.
|
|
793
|
-
const f = fakeFetch({ ok: true, result: { members: [] } });
|
|
794
|
-
const res = await executeOrgTool(
|
|
795
|
-
"engage_colleagues",
|
|
796
|
-
{ id: "W-1", title: "Refresh the digest", state: "in_progress", interestedParties: ["M-2"] },
|
|
797
|
-
{ orgConfig: CFG, agentRoot: "/tmp/engage-tool-test", fetchImpl: f, env: {} },
|
|
798
|
-
);
|
|
799
|
-
assert.equal(res.ok, true);
|
|
800
|
-
assert.equal(res.result.engaged, false);
|
|
801
|
-
assert.equal(res.result.reasonCode, "self_contained");
|
|
802
|
-
assert.deepEqual(res.result.consideredNotEngaged, ["M-2"]);
|
|
803
|
-
assert.ok(res.result.why.length, "the reasoning comes back to the model, not just the verdict");
|
|
804
|
-
assert.ok(
|
|
805
|
-
!f.calls.some((c) => /messaging\.send/.test(c.url)),
|
|
806
|
-
"a self-contained decision must not put a message on the wire",
|
|
807
|
-
);
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
test("engage_colleagues refuses an unidentified piece of work rather than guessing", async () => {
|
|
811
|
-
const res = await executeOrgTool(
|
|
812
|
-
"engage_colleagues",
|
|
813
|
-
{ title: "no id" },
|
|
814
|
-
{ orgConfig: CFG, agentRoot: "/tmp/engage-tool-test", fetchImpl: fakeFetch(), env: {} },
|
|
815
|
-
);
|
|
816
|
-
assert.equal(res.ok, false);
|
|
817
|
-
assert.equal(res.error.code, "BAD_REQUEST");
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
test("messaging_send passes mentions straight through to the wire", async () => {
|
|
821
|
-
const f = fakeFetch({ ok: true, result: { id: "m1" } });
|
|
822
|
-
await executeOrgTool(
|
|
823
|
-
"messaging_send",
|
|
824
|
-
{ channelId: "C-1", body: "Need a decision here.", mentions: ["M-2"] },
|
|
825
|
-
{ orgConfig: CFG, agentRoot: "/tmp", fetchImpl: f, env: {}, screenImpl: async () => ({ allow: true }) },
|
|
826
|
-
);
|
|
827
|
-
const body = JSON.parse(f.calls[0].init.body);
|
|
828
|
-
// The model writes the obvious thing — an array of id strings — and the param
|
|
829
|
-
// contract coerces it at the one chokepoint. Sending the string form verbatim
|
|
830
|
-
// fails hq's zod and 400s the WHOLE message, tags and body alike.
|
|
831
|
-
assert.deepEqual(body.mentions, [{ memberId: "M-2" }]);
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
// ── speaking: messaging_send_voice_note ────────────────────────────────────
|
|
835
|
-
//
|
|
836
|
-
// The verb that closes "I can't send audio voice notes". hq could always
|
|
837
|
-
// synthesise an agent's voice note, but only as a REFLEX answering a human's
|
|
838
|
-
// spoken note in kind — asked in text, an agent had nothing to call.
|
|
839
|
-
//
|
|
840
|
-
// What is pinned here is the composition: record, then post the recording as an
|
|
841
|
-
// ORDINARY message attachment. There is no audio-only send path, so the post
|
|
842
|
-
// leg carries the same ACL, audit and dedup as any typed message; and a refused
|
|
843
|
-
// recording must never be followed by a post that implies one was made.
|
|
844
|
-
|
|
845
|
-
test("messaging_send_voice_note: records, then posts the file as an ordinary attachment", async () => {
|
|
846
|
-
let n = 0;
|
|
847
|
-
const calls = [];
|
|
848
|
-
const fetchImpl = async (url, init) => {
|
|
849
|
-
calls.push({ url: String(url), body: JSON.parse(init.body) });
|
|
850
|
-
n += 1;
|
|
851
|
-
return {
|
|
852
|
-
ok: true,
|
|
853
|
-
status: 200,
|
|
854
|
-
headers: { get: () => undefined },
|
|
855
|
-
json: async () =>
|
|
856
|
-
n === 1
|
|
857
|
-
? { ok: true, result: { fileId: "file_v1", durationMs: 4200 } }
|
|
858
|
-
: { ok: true, result: { messageId: "m1" } },
|
|
859
|
-
};
|
|
860
|
-
};
|
|
861
|
-
const frame = await executeOrgTool(
|
|
862
|
-
"messaging_send_voice_note",
|
|
863
|
-
{ channelId: "C1", text: "Shipping Friday." },
|
|
864
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
|
|
865
|
-
);
|
|
866
|
-
assert.equal(frame.ok, true);
|
|
867
|
-
assert.match(calls[0].url, /messaging\.synthesizeVoiceNote$/);
|
|
868
|
-
assert.equal(calls[0].body.text, "Shipping Friday.");
|
|
869
|
-
assert.match(calls[1].url, /messaging\.send$/);
|
|
870
|
-
assert.deepEqual(calls[1].body.attachments, [{ fileId: "file_v1" }]);
|
|
871
|
-
// The written body defaults to the spoken words, never an empty message.
|
|
872
|
-
assert.equal(calls[1].body.body, "Shipping Friday.");
|
|
873
|
-
assert.ok(calls[1].body.idempotencyId, "the post is dedup-keyed like any send");
|
|
874
|
-
assert.equal(frame.result.fileId, "file_v1");
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
test("messaging_send_voice_note: a refused recording is NOT followed by a post", async () => {
|
|
878
|
-
const calls = [];
|
|
879
|
-
const fetchImpl = async (url, init) => {
|
|
880
|
-
calls.push(String(url));
|
|
881
|
-
return {
|
|
882
|
-
ok: false,
|
|
883
|
-
status: 403,
|
|
884
|
-
headers: { get: () => undefined },
|
|
885
|
-
json: async () => ({
|
|
886
|
-
ok: false,
|
|
887
|
-
error: { code: "FORBIDDEN_SCOPE", message: "voice note not recorded (no-key): ..." },
|
|
888
|
-
}),
|
|
889
|
-
};
|
|
890
|
-
};
|
|
891
|
-
const frame = await executeOrgTool(
|
|
892
|
-
"messaging_send_voice_note",
|
|
893
|
-
{ channelId: "C1", text: "hi" },
|
|
894
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
|
|
895
|
-
);
|
|
896
|
-
assert.equal(frame.ok, false);
|
|
897
|
-
assert.match(frame.error.message, /no-key/);
|
|
898
|
-
assert.equal(calls.length, 1, "no post claiming a voice note that was never recorded");
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
test("messaging_send_voice_note: the send-gate screens the SPOKEN words before synthesis", async () => {
|
|
902
|
-
const fetchImpl = fakeFetch();
|
|
903
|
-
const seen = [];
|
|
904
|
-
const frame = await executeOrgTool(
|
|
905
|
-
"messaging_send_voice_note",
|
|
906
|
-
{ channelId: "C1", text: "the secret is hunter2", body: "see note" },
|
|
907
|
-
{
|
|
908
|
-
orgConfig: CFG,
|
|
909
|
-
agentRoot: "/tmp/none",
|
|
910
|
-
fetchImpl,
|
|
911
|
-
screenImpl: async (args) => {
|
|
912
|
-
seen.push(args);
|
|
913
|
-
return args.text.includes("hunter2") ? { allow: false, reason: "credential" } : { allow: true };
|
|
914
|
-
},
|
|
915
|
-
},
|
|
916
|
-
);
|
|
917
|
-
assert.equal(frame.ok, false);
|
|
918
|
-
assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
|
|
919
|
-
// The gate sees the PROSE — the spoken words and the written body — not a
|
|
920
|
-
// stringified params blob. A gate judging JSON would pass sentences it never
|
|
921
|
-
// actually read.
|
|
922
|
-
assert.equal(seen[0].text, "the secret is hunter2\nsee note");
|
|
923
|
-
assert.equal(seen[0].recipient, "C1");
|
|
924
|
-
// Blocked BEFORE the network: audio is the hardest content to retract, and a
|
|
925
|
-
// blocked note must not cost a billable synthesis either.
|
|
926
|
-
assert.equal(fetchImpl.calls.length, 0);
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
test("messaging_send_voice_note: channelId and text are both required", async () => {
|
|
930
|
-
const fetchImpl = fakeFetch();
|
|
931
|
-
const frame = await executeOrgTool(
|
|
932
|
-
"messaging_send_voice_note",
|
|
933
|
-
{ channelId: "C1" },
|
|
934
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
|
|
935
|
-
);
|
|
936
|
-
assert.equal(frame.ok, false);
|
|
937
|
-
assert.equal(frame.error.code, "BAD_REQUEST");
|
|
938
|
-
assert.equal(fetchImpl.calls.length, 0);
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
// ── mandate / sub-agent registry / preferences (2026-08 parity delta) ───────
|
|
942
|
-
|
|
943
|
-
test("mandate tools ride their mandate.* methods and POST the params verbatim", async () => {
|
|
944
|
-
const seen = [];
|
|
945
|
-
const fetchImpl = async (url, init) => {
|
|
946
|
-
seen.push({ url: String(url), body: JSON.parse(init.body) });
|
|
947
|
-
return { ok: true, status: 200, headers: { get: () => undefined }, json: async () => ({ ok: true, result: {} }) };
|
|
948
|
-
};
|
|
949
|
-
fetchImpl.calls = seen;
|
|
950
|
-
const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
|
|
951
|
-
await executeOrgTool("mandate_read", {}, o);
|
|
952
|
-
await executeOrgTool("mandate_objectives", { state: "active", includeRetired: false }, o);
|
|
953
|
-
await executeOrgTool("mandate_kpi_series", { objectiveId: "ob-1", limit: 20 }, o);
|
|
954
|
-
await executeOrgTool("mandate_propose_objective", { key: "pipeline-coverage", text: "3x cover", target: 3 }, o);
|
|
955
|
-
await executeOrgTool(
|
|
956
|
-
"mandate_record_kpi_sample",
|
|
957
|
-
{ objectiveId: "ob-1", value: 2.4, window: "2026-W33", evidence: { method: "crm.listDeals" } },
|
|
958
|
-
o,
|
|
959
|
-
);
|
|
960
|
-
assert.deepEqual(seen.map((c) => c.url), [
|
|
961
|
-
"https://org.example/api/v1/mandate.get",
|
|
962
|
-
"https://org.example/api/v1/mandate.tree",
|
|
963
|
-
"https://org.example/api/v1/mandate.series",
|
|
964
|
-
"https://org.example/api/v1/mandate.propose",
|
|
965
|
-
"https://org.example/api/v1/mandate.sample",
|
|
966
|
-
]);
|
|
967
|
-
// Pass-through, byte for byte: hq's mandate schemas read these names, and the
|
|
968
|
-
// evidence OBJECT is what the server's honesty gate inspects — flattening it
|
|
969
|
-
// to a string here would silently downgrade every sample to `llm`.
|
|
970
|
-
assert.deepEqual(seen[3].body, { key: "pipeline-coverage", text: "3x cover", target: 3 });
|
|
971
|
-
assert.deepEqual(seen[4].body.evidence, { method: "crm.listDeals" });
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
test("the mandate belt stops at PROPOSE — no adopt/retire tool exists at any tier", () => {
|
|
975
|
-
const names = new Set(ORG_TOOLS.map((t) => t.name));
|
|
976
|
-
for (const forbidden of ["mandate_adopt", "mandate_retire"]) {
|
|
977
|
-
assert.equal(names.has(forbidden), false, `${forbidden} must not exist`);
|
|
978
|
-
}
|
|
979
|
-
// The absence is structural, not incidental: no curated tool may bind a
|
|
980
|
-
// method whose scope is missing from DEFAULT_AGENT_SCOPES, because a tool the
|
|
981
|
-
// seat's own credential can never execute is a promise the model will make
|
|
982
|
-
// and fail to keep. `mandate.write` is the anti-Goodhart scope.
|
|
983
|
-
const bound = ORG_TOOLS.filter((t) => t.binding.kind === "rpc").map((t) => t.binding.method);
|
|
984
|
-
for (const m of bound) {
|
|
985
|
-
assert.notEqual(methodDef(m).scope, "mandate.write", `${m} needs mandate.write — not an agent's to hold`);
|
|
986
|
-
}
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
test("subagent tools are READS ONLY — the writes stay with the outbox-owning client", () => {
|
|
990
|
-
const subagentTools = ORG_TOOLS.filter(
|
|
991
|
-
(t) => t.binding.kind === "rpc" && t.binding.method.startsWith("subagent."),
|
|
992
|
-
);
|
|
993
|
-
assert.deepEqual(
|
|
994
|
-
subagentTools.map((t) => t.name).sort(),
|
|
995
|
-
["subagent_get", "subagent_list", "subagent_resolve"],
|
|
996
|
-
);
|
|
997
|
-
for (const t of subagentTools) {
|
|
998
|
-
assert.equal(t.access, "read", `${t.name} is a read`);
|
|
999
|
-
assert.equal(methodDef(t.binding.method).sideEffecting, false, `${t.binding.method} mutates nothing`);
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
test("preference tools ship as a PAIR — a write-only memory is not parity", async () => {
|
|
1004
|
-
const names = ORG_TOOLS.filter(
|
|
1005
|
-
(t) => t.binding.kind === "rpc" && t.binding.method.startsWith("preference."),
|
|
1006
|
-
);
|
|
1007
|
-
assert.deepEqual(names.map((t) => t.name).sort(), ["preference_list", "preference_remember"]);
|
|
1008
|
-
assert.equal(orgToolDef("preference_list").access, "read");
|
|
1009
|
-
assert.equal(orgToolDef("preference_remember").access, "write");
|
|
1010
|
-
// `why` is required in the SCHEMA, not just server-side: the evidence rule
|
|
1011
|
-
// has to reach the model, or it learns the rule by BAD_REQUEST.
|
|
1012
|
-
assert.ok(orgToolDef("preference_remember").input_schema.required.includes("why"));
|
|
1013
|
-
|
|
1014
|
-
const fetchImpl = fakeFetch({ ok: true, result: { id: "pref-1" } });
|
|
1015
|
-
const frame = await executeOrgTool(
|
|
1016
|
-
"preference_remember",
|
|
1017
|
-
{ domain: "travel", key: "seat", value: "aisle", why: "said so in #travel" },
|
|
1018
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
1019
|
-
);
|
|
1020
|
-
assert.equal(frame.ok, true);
|
|
1021
|
-
assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/preference.upsert");
|
|
1022
|
-
// sideEffecting:true at the dispatcher → a fresh header key per invocation.
|
|
1023
|
-
assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "dispatcher key attached");
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
// ── the access mechanism, TABLE-WIDE ───────────────────────────────────────
|
|
1027
|
-
//
|
|
1028
|
-
// The per-delta assertion below ("the new tools reach the tiers that need
|
|
1029
|
-
// them") lists ten names by hand, so it polices the tools whose author
|
|
1030
|
-
// remembered to add them and nothing else — every family that ships after it is
|
|
1031
|
-
// unguarded by default. These three assertions are the table-wide version:
|
|
1032
|
-
// they iterate ORG_TOOLS, so a capability cannot be parked at the CEO-only tier
|
|
1033
|
-
// or given a tier its protocol scope contradicts without one of them going red,
|
|
1034
|
-
// whether or not anyone updates a list.
|
|
1035
|
-
|
|
1036
|
-
test("ACCESS MECHANISM: `admin` is a CLOSED set of two escape hatches, not a parking space", () => {
|
|
1037
|
-
const admins = ORG_TOOLS.filter((t) => t.access === "admin").map((t) => t.name).sort();
|
|
1038
|
-
assert.deepEqual(
|
|
1039
|
-
admins,
|
|
1040
|
-
[...ADMIN_TOOLS].sort(),
|
|
1041
|
-
"only org_rpc/org_read may be admin-tier — anything else is a CAPABILITY, and a capability " +
|
|
1042
|
-
"at admin is CEO-only, i.e. exactly as unreachable for a leadership or default agent as " +
|
|
1043
|
-
"the org_rpc route a curated tool exists to replace",
|
|
1044
|
-
);
|
|
1045
|
-
for (const name of ADMIN_TOOLS) {
|
|
1046
|
-
const def = orgToolDef(name);
|
|
1047
|
-
assert.ok(def, `${name} is in the table`);
|
|
1048
|
-
assert.equal(def.access, "admin", `${name} is the escape hatch it claims to be`);
|
|
1049
|
-
// Both hatches are `local`: they take a method/path NAME and validate it
|
|
1050
|
-
// against the frozen table before any I/O. A desk verb can never be one.
|
|
1051
|
-
assert.equal(def.binding.kind, "local", `${name} is a console, not a capability`);
|
|
1052
|
-
}
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
test("ACCESS MECHANISM: every rpc-bound tool's tier is DERIVED from its protocol scope", () => {
|
|
1056
|
-
// hq gates on SCOPE, so the native clamp has to mirror the scope lane or it
|
|
1057
|
-
// either withholds a tool hq would have allowed or offers one hq will refuse.
|
|
1058
|
-
// Note the rule this is NOT: `sideEffecting ? "write" : "read"` is wrong for
|
|
1059
|
-
// seven tools already in the table — knowledge.search, directory.ask and the
|
|
1060
|
-
// three branding renders/exports are AUDITED READS (side-effecting, `.read`
|
|
1061
|
-
// scope, runnable by a VIEWER seat), and artifact.create/act are the mirror
|
|
1062
|
-
// case. Deriving from sideEffecting would demote all five audited reads out
|
|
1063
|
-
// of the lookup set every tier gets.
|
|
1064
|
-
let checked = 0;
|
|
1065
|
-
for (const t of ORG_TOOLS) {
|
|
1066
|
-
if (!t.binding || t.binding.kind !== "rpc") continue;
|
|
1067
|
-
checked += 1;
|
|
1068
|
-
const want = expectedAccessFor(t.binding.method);
|
|
1069
|
-
assert.ok(want, `${t.name} binds a vendored method`);
|
|
1070
|
-
assert.equal(
|
|
1071
|
-
t.access,
|
|
1072
|
-
want,
|
|
1073
|
-
`${t.name} (${t.binding.method}, scope ${methodDef(t.binding.method).scope}) must be access:"${want}"`,
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
assert.ok(checked >= 120, "the whole rpc-bound belt was checked, not a sample");
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
test("ACCESS MECHANISM: no tool can fall out of every tier — the partition is exhaustive", () => {
|
|
1080
|
-
// Exact string equality used to bucket the table, so a missing or mis-cased
|
|
1081
|
-
// `access` matched NO bucket and reached NO native access level, while the
|
|
1082
|
-
// MCP plane kept publishing it. normalizeToolAccess is the tool-side twin of
|
|
1083
|
-
// normalizeAccessLevel: it normalises casing and sends a genuinely
|
|
1084
|
-
// unrecognised tier to the most-restricted bucket, loudly.
|
|
1085
|
-
for (const t of ORG_TOOLS) {
|
|
1086
|
-
assert.ok(
|
|
1087
|
-
["read", "write", "admin"].includes(t.access),
|
|
1088
|
-
`${t.name} declares a known tier verbatim (the normaliser is a safety net, not a licence)`,
|
|
1089
|
-
);
|
|
1090
|
-
const warnings = [];
|
|
1091
|
-
assert.equal(normalizeToolAccess(t.access, t.name, (m) => warnings.push(m)), t.access);
|
|
1092
|
-
assert.equal(warnings.length, 0, `${t.name} normalises silently`);
|
|
1093
|
-
}
|
|
1094
|
-
// And the safety net itself: a typo fails CLOSED and NOISY, never invisible.
|
|
1095
|
-
const warnings = [];
|
|
1096
|
-
assert.equal(normalizeToolAccess("Read", "x_tool", (m) => warnings.push(m)), "read", "casing is forgiven");
|
|
1097
|
-
assert.equal(normalizeToolAccess(undefined, "x_tool", (m) => warnings.push(m)), "admin", "a missing tier fails closed");
|
|
1098
|
-
assert.equal(warnings.length, 1, "and says so out loud");
|
|
1099
|
-
assert.match(warnings[0], /x_tool/);
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
test("the new tools reach the tiers that need them — reads everywhere, no new admin surface", () => {
|
|
1103
|
-
const added = [
|
|
1104
|
-
"mandate_read", "mandate_objectives", "mandate_kpi_series",
|
|
1105
|
-
"mandate_propose_objective", "mandate_record_kpi_sample",
|
|
1106
|
-
"subagent_list", "subagent_get", "subagent_resolve",
|
|
1107
|
-
"preference_remember", "preference_list",
|
|
1108
|
-
];
|
|
1109
|
-
for (const n of added) {
|
|
1110
|
-
const def = orgToolDef(n);
|
|
1111
|
-
assert.ok(def, `${n} is in the table`);
|
|
1112
|
-
// The whole point of the delta: NONE of these may land at access:"admin",
|
|
1113
|
-
// which is the CEO-only escape-hatch tier. A capability parked there is
|
|
1114
|
-
// exactly as unreachable as the org_rpc route it was meant to replace.
|
|
1115
|
-
assert.notEqual(def.access, "admin", `${n} must not be admin-tier`);
|
|
1116
|
-
assert.equal(def.outbound, undefined, `${n} sends nothing a human reads as a message`);
|
|
1117
|
-
}
|
|
1118
|
-
assert.equal(
|
|
1119
|
-
added.filter((n) => orgToolDef(n).access === "read").length,
|
|
1120
|
-
7,
|
|
1121
|
-
"seven reads (3 mandate, 3 subagent, 1 preference) land in EVERY tier's lookup set",
|
|
1122
|
-
);
|
|
1123
|
-
});
|
|
1124
|
-
|
|
1125
|
-
test("the new tools FAIL CLOSED with no credential — refusal, never a silent success", async () => {
|
|
1126
|
-
const fetchImpl = fakeFetch();
|
|
1127
|
-
for (const n of ["mandate_read", "subagent_list", "preference_remember"]) {
|
|
1128
|
-
const frame = await executeOrgTool(n, {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
|
|
1129
|
-
assert.equal(frame.ok, false, `${n} refuses`);
|
|
1130
|
-
assert.equal(frame.error.code, "UNAUTHORIZED");
|
|
1131
|
-
assert.match(frame.error.message, /COHORT_API_TOKEN/);
|
|
1132
|
-
}
|
|
1133
|
-
assert.equal(fetchImpl.calls.length, 0, "nothing dispatched without a credential");
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
test("a 401/403 from hq surfaces VERBATIM on the new tools (a refusal is an answer)", async () => {
|
|
1137
|
-
const denied = fakeFetch(
|
|
1138
|
-
{ ok: false, error: { code: "FORBIDDEN_SCOPE", message: "resolving another member's sub-agents requires admin" } },
|
|
1139
|
-
{ ok: false, status: 403 },
|
|
1140
|
-
);
|
|
1141
|
-
const frame = await executeOrgTool(
|
|
1142
|
-
"subagent_resolve",
|
|
1143
|
-
{ memberId: "someone-else" },
|
|
1144
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: denied },
|
|
1145
|
-
);
|
|
1146
|
-
assert.equal(frame.ok, false);
|
|
1147
|
-
assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
|
|
1148
|
-
assert.match(frame.error.message, /admin/);
|
|
1149
|
-
|
|
1150
|
-
const unauth = fakeFetch({ ok: false, error: { code: "UNAUTHORIZED", message: "bad key" } }, { ok: false, status: 401 });
|
|
1151
|
-
const f2 = await executeOrgTool("mandate_read", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: unauth });
|
|
1152
|
-
assert.equal(f2.ok, false);
|
|
1153
|
-
assert.equal(f2.error.code, "UNAUTHORIZED");
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
test("transport failure on a new tool fails open into an INTERNAL frame, never a throw", async () => {
|
|
1157
|
-
const thrower = async () => { throw new Error("econnrefused"); };
|
|
1158
|
-
const frame = await executeOrgTool(
|
|
1159
|
-
"mandate_objectives",
|
|
1160
|
-
{},
|
|
1161
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: thrower },
|
|
1162
|
-
);
|
|
1163
|
-
assert.equal(frame.ok, false);
|
|
1164
|
-
assert.equal(frame.error.code, "INTERNAL");
|
|
1165
|
-
});
|
|
1166
|
-
|
|
1167
|
-
// ── WP-M4: front-door tools (board_mine / board_track / session_status) ────
|
|
1168
|
-
|
|
1169
|
-
test("board_mine: read access, GET /v1/board.mine, items normalised, [] when hq lacks the read (fail-open)", async () => {
|
|
1170
|
-
const def = orgToolDef("board_mine");
|
|
1171
|
-
assert.ok(def, "board_mine curated");
|
|
1172
|
-
assert.equal(def.access, "read");
|
|
1173
|
-
assert.equal(def.binding.kind, "local", "local so an hq without board.mine yet degrades to [] instead of a bare 404");
|
|
1174
|
-
const fetchImpl = fakeFetch({ items: [{ itemId: "i1", title: "Deck", col: "doing" }] });
|
|
1175
|
-
const frame = await executeOrgTool("board_mine", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
1176
|
-
assert.equal(frame.ok, true);
|
|
1177
|
-
assert.deepEqual(frame.result.items, [{ itemId: "i1", title: "Deck", col: "doing" }]);
|
|
1178
|
-
assert.equal(frame.result.count, 1);
|
|
1179
|
-
assert.match(fetchImpl.calls[0].url, /\/v1\/board\.mine$/);
|
|
1180
|
-
assert.equal(fetchImpl.calls[0].init.method, "GET");
|
|
1181
|
-
const gone = fakeFetch({ error: { code: "NOT_FOUND", message: "no such read" } }, { status: 404, ok: false });
|
|
1182
|
-
const f2 = await executeOrgTool("board_mine", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gone });
|
|
1183
|
-
assert.equal(f2.ok, true, "fail-open: an older hq is an empty list, not an error the model must interpret");
|
|
1184
|
-
assert.deepEqual(f2.result.items, []);
|
|
1185
|
-
// No token → UNAUTHORIZED like every other network tool.
|
|
1186
|
-
const noTok = await executeOrgTool("board_mine", {}, { orgConfig: { org: { cohort: { enabled: true, base: "https://org.example" } } }, agentRoot: "/tmp/none", fetchImpl });
|
|
1187
|
-
assert.equal(noTok.ok, false);
|
|
1188
|
-
assert.equal(noTok.error.code, "UNAUTHORIZED");
|
|
1189
|
-
});
|
|
1190
|
-
|
|
1191
|
-
test("board_track: write access on board.track; title/why/stage pass through; whole ladder incl. accepted|done; bad stage rejected offline", async () => {
|
|
1192
|
-
const def = orgToolDef("board_track");
|
|
1193
|
-
assert.ok(def, "board_track curated");
|
|
1194
|
-
assert.equal(def.access, "write");
|
|
1195
|
-
assert.equal(def.binding.method, "board.track");
|
|
1196
|
-
assert.deepEqual(def.input_schema.properties.stage.enum, ["accepted", "working", "blocked", "review", "done", "failed"]);
|
|
1197
|
-
assert.ok(def.input_schema.properties.title, "title passthrough");
|
|
1198
|
-
assert.ok(def.input_schema.properties.why, "why passthrough");
|
|
1199
|
-
const fetchImpl = fakeFetch({ ok: true, result: { tracked: true, taskId: "t1", col: "todo", created: true } });
|
|
1200
|
-
const frame = await executeOrgTool(
|
|
1201
|
-
"board_track",
|
|
1202
|
-
{ channelId: "c1", messageId: "m1", stage: "accepted", title: "Build the deck", why: "asked in DM", note: "on it" },
|
|
1203
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
1204
|
-
);
|
|
1205
|
-
assert.equal(frame.ok, true);
|
|
1206
|
-
assert.equal(frame.result.taskId, "t1");
|
|
1207
|
-
assert.match(fetchImpl.calls[0].url, /\/v1\/board\.track$/);
|
|
1208
|
-
const body = JSON.parse(fetchImpl.calls[0].init.body);
|
|
1209
|
-
assert.equal(body.stage, "accepted");
|
|
1210
|
-
assert.equal(body.title, "Build the deck");
|
|
1211
|
-
assert.equal(body.why, "asked in DM");
|
|
1212
|
-
assert.equal(body.service, "cohort", "service defaults to cohort");
|
|
1213
|
-
assert.equal(body.channelId, "c1");
|
|
1214
|
-
assert.equal(body.messageId, "m1");
|
|
1215
|
-
assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], "board.track:c1:m1:accepted", "stable per (ask, stage)");
|
|
1216
|
-
const bad = fakeFetch();
|
|
1217
|
-
const rej = await executeOrgTool("board_track", { channelId: "c1", messageId: "m1", stage: "later" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: bad });
|
|
1218
|
-
assert.equal(rej.ok, false);
|
|
1219
|
-
assert.equal(rej.error.code, "BAD_REQUEST");
|
|
1220
|
-
assert.equal(bad.calls.length, 0, "validated before any network");
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
test("session_status: offline local read of state/session + state/org/board-mine; works with no token", async () => {
|
|
1224
|
-
const def = orgToolDef("session_status");
|
|
1225
|
-
assert.ok(def, "session_status curated");
|
|
1226
|
-
assert.equal(def.access, "read");
|
|
1227
|
-
assert.equal(def.binding.kind, "local");
|
|
1228
|
-
const root = mkdtempSync(join(tmpdir(), "ts-session-"));
|
|
1229
|
-
try {
|
|
1230
|
-
mkdirSync(join(root, "state", "session", "handoffs"), { recursive: true });
|
|
1231
|
-
mkdirSync(join(root, "state", "org"), { recursive: true });
|
|
1232
|
-
writeFileSync(join(root, "state", "session", "heartbeat.json"), JSON.stringify({ pid: 1, name: "alex-main", sessionId: "s1", ts: Date.now() }));
|
|
1233
|
-
writeFileSync(join(root, "state", "session", "peers.json"), JSON.stringify([{ name: "alex-deck", purpose: "board deck" }]));
|
|
1234
|
-
writeFileSync(join(root, "state", "session", "handoffs", "t1.json"), "{}");
|
|
1235
|
-
writeFileSync(join(root, "state", "org", "board-mine.json"), JSON.stringify({ items: [{ itemId: "i1" }, { itemId: "i2" }], fetchedAt: new Date().toISOString() }));
|
|
1236
|
-
const fetchImpl = fakeFetch();
|
|
1237
|
-
const frame = await executeOrgTool("session_status", {}, { orgConfig: { org: { cohort: { enabled: true, base: "https://org.example" } } }, agentRoot: root, fetchImpl });
|
|
1238
|
-
assert.equal(frame.ok, true, "no token is fine — this never touches the network");
|
|
1239
|
-
assert.equal(fetchImpl.calls.length, 0);
|
|
1240
|
-
assert.equal(frame.result.live, true);
|
|
1241
|
-
assert.equal(frame.result.name, "alex-main");
|
|
1242
|
-
assert.equal(frame.result.peers[0].name, "alex-deck");
|
|
1243
|
-
assert.equal(frame.result.handoffsOpen, 1);
|
|
1244
|
-
assert.equal(frame.result.boardMineCount, 2);
|
|
1245
|
-
assert.equal(typeof frame.result.summary, "string");
|
|
1246
|
-
// An empty root is "absent", not an error.
|
|
1247
|
-
const empty = await executeOrgTool("session_status", {}, { orgConfig: CFG, agentRoot: join(root, "nope"), fetchImpl });
|
|
1248
|
-
assert.equal(empty.ok, true);
|
|
1249
|
-
assert.equal(empty.result.state, "absent");
|
|
1250
|
-
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
|
-
test("board_track: the model cannot override service or smuggle extra keys — only the schema's passthrough fields reach hq", async () => {
|
|
1254
|
-
const fetchImpl = fakeFetch({ ok: true, result: { tracked: true, taskId: "t2", col: "doing" } });
|
|
1255
|
-
const frame = await executeOrgTool(
|
|
1256
|
-
"board_track",
|
|
1257
|
-
{ channelId: "c1", messageId: "m1", stage: "working", service: "slack", junk: "x", note: "halfway", priority: "P1", notify: ["u1"] },
|
|
1258
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
1259
|
-
);
|
|
1260
|
-
assert.equal(frame.ok, true);
|
|
1261
|
-
const body = JSON.parse(fetchImpl.calls[0].init.body);
|
|
1262
|
-
assert.equal(body.service, "cohort", "service is pinned, not model-supplied");
|
|
1263
|
-
assert.equal(body.junk, undefined, "unknown keys are dropped");
|
|
1264
|
-
assert.equal(body.note, "halfway");
|
|
1265
|
-
assert.equal(body.priority, "P1");
|
|
1266
|
-
assert.deepEqual(body.notify, ["u1"]);
|
|
1267
|
-
assert.deepEqual(Object.keys(body).sort(), ["channelId", "messageId", "note", "notify", "priority", "service", "stage"]);
|
|
1268
|
-
});
|