@desplega.ai/agent-swarm 1.164.1 → 1.166.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/acp-adapter-yzfxn8vj.js +23 -0
- package/dist/{actions-hp2h32dx.js → actions-qz3m692w.js} +24 -28
- package/dist/{amp-adapter-zfd7djyx.js → amp-adapter-j1xbmsqz.js} +7 -7
- package/dist/{app-w2d26h4f.js → app-43vrdmv8.js} +9 -10
- package/dist/{assistant-45pc4en7.js → assistant-ertffmka.js} +25 -29
- package/dist/{boot-reembed-k1q2s4fc.js → boot-reembed-998jtpdg.js} +12 -15
- package/dist/{boot-reembed-6rhcpsjf.js → boot-reembed-s7k7c6v2.js} +13 -15
- package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
- package/dist/{boot-seeds-ex93edx7.js → boot-seeds-vvzs1zhk.js} +12 -12
- package/dist/{claude-adapter-hcxd0szm.js → claude-adapter-2zvmekrp.js} +15 -17
- package/dist/{claude-managed-adapter-fz7xps2r.js → claude-managed-adapter-s49wbk8t.js} +5 -5
- package/dist/{claude-managed-setup-xx6f15h9.js → claude-managed-setup-cvhqq8qn.js} +4 -4
- package/dist/{claude-sdk-session-6nb9raem.js → claude-sdk-session-kkbxtw5q.js} +2624 -2606
- package/dist/{cli-etbrt8x1.js → cli-0nhdwxd5.js} +4 -4
- package/dist/{cli-jeefgppq.js → cli-0qjcsxgz.js} +7 -7
- package/dist/{cli-6behs2bt.js → cli-32bca7nr.js} +1 -1
- package/dist/{cli-gt8v96qx.js → cli-3ddybbbx.js} +4 -4
- package/dist/{acp-adapter-rxvek4ym.js → cli-3qn4r334.js} +317 -35
- package/dist/{cli-1d3w4xm3.js → cli-4r1sjfc4.js} +14 -6
- package/dist/{cli-691f2rxm.js → cli-66zh26sp.js} +68 -16
- package/dist/{cli-et8wdyqh.js → cli-68r1j4sg.js} +9 -9
- package/dist/{cli-xb7kyanp.js → cli-8kgvmkak.js} +3 -3
- package/dist/{cli-fdpkw8vk.js → cli-8t229cmr.js} +8 -8
- package/dist/{cli-gc54qc0p.js → cli-aqcey741.js} +1 -1
- package/dist/{cli-4q4cmr4t.js → cli-cpfmqz3n.js} +8 -8
- package/dist/{cli-a9bw0xxt.js → cli-df56tqdw.js} +997 -408
- package/dist/{cli-n7h1kqpf.js → cli-dmtrxqhg.js} +6 -1
- package/dist/{cli-xfb7333f.js → cli-dw2q9f7g.js} +5 -5
- package/dist/{cli-4drstbb9.js → cli-ecknp7p1.js} +81 -23
- package/dist/{cli-v0pvvhfq.js → cli-efnd2kce.js} +36 -1
- package/dist/{cli-1wetf34q.js → cli-f2n17zhp.js} +1 -1
- package/dist/{cli-2c8eky5y.js → cli-feh3dkah.js} +1 -1
- package/dist/cli-fn697bnt.js +126 -0
- package/dist/{cli-v5pr2fgt.js → cli-gt8azyet.js} +3 -3
- package/dist/{cli-dbszf3rw.js → cli-h3x613ze.js} +4 -4
- package/dist/{cli-nwhb8y2e.js → cli-hf5s9pgf.js} +1 -1
- package/dist/cli-j2jbr8af.js +516 -0
- package/dist/{cli-agjzafns.js → cli-jhm6hzjn.js} +593 -10
- package/dist/{cli-j0tmc908.js → cli-jvj6a0cv.js} +484 -477
- package/dist/{cli-xxz89c2n.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-07s39g1n.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-v90vfwyb.js → cli-mwegs4af.js} +5 -5
- package/dist/{cli-bz1gbac9.js → cli-p4ztb80h.js} +2 -2
- package/dist/{cli-1wd1n2ya.js → cli-rfphkzh5.js} +1 -1
- package/dist/{cli-976r1zqx.js → cli-rggdq8wq.js} +5 -5
- package/dist/{cli-3ajw5v5v.js → cli-rsy933yz.js} +6 -6
- package/dist/{cli-yj790e19.js → cli-tadfgd0t.js} +8 -1
- package/dist/{cli-cn53e7vc.js → cli-ttbsse3f.js} +6 -1
- package/dist/{cli-dyqsrqcf.js → cli-v2a8x45d.js} +4 -4
- package/dist/{cli-wbvnn6tk.js → cli-v6vv7dy8.js} +91 -9
- package/dist/{cli-z2ap2c0c.js → cli-vzc73wet.js} +90 -55
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-4cn19jvn.js → cli-x7kpr72a.js} +3 -3
- package/dist/{cli-003785v9.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-v0e7jpr5.js → cli-xvdgasgy.js} +13 -4
- package/dist/{cli-asqeg4ve.js → cli-y59h2pk9.js} +2 -2
- package/dist/{cli-0wn3ypqv.js → cli-yshaazs1.js} +14121 -3478
- package/dist/{cli-wg8yjqtw.js → cli-ysnyhws9.js} +16 -3
- package/dist/cli-zp67qg5x.js +92 -0
- package/dist/{cli-g3qgrzck.js → cli-zsh093hk.js} +71 -3
- package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
- package/dist/cli.js +79 -26
- package/dist/{codex-adapter-pcxdn10v.js → codex-adapter-dw1wxqt8.js} +10 -10
- package/dist/{codex-hook-b1fb7c2k.js → codex-hook-d2rkvf39.js} +1 -1
- package/dist/{codex-login-059nm145.js → codex-login-terbdsqq.js} +2 -2
- package/dist/{codex-session-runner-xzwwddp8.js → codex-session-runner-3cgb8y1g.js} +10 -10
- package/dist/{commands-98at8mkn.js → commands-mhxam6z2.js} +9 -9
- package/dist/{cursor-adapter-dkg6qnkp.js → cursor-adapter-2qda3st7.js} +6 -6
- package/dist/{db-01g8wtvw.js → db-sjks418k.js} +11 -9
- package/dist/{devin-adapter-qmttt2fz.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-nn2k1wxd.js → dispatcher-1es759rn.js} +22 -26
- package/dist/{dsh-adapter-w4d4vwdq.js → dsh-adapter-bek4cyw9.js} +6 -6
- package/dist/{e2b-9e4g2hvt.js → e2b-nd50eccg.js} +1 -1
- package/dist/{event-bus-z7v2t26e.js → event-bus-vnfthafg.js} +2 -2
- package/dist/grok-adapter-ar9cw9jg.js +29 -0
- package/dist/{handlers-b0kqsbg8.js → handlers-1gkbhktr.js} +25 -29
- package/dist/{hook-sap7v1aj.js → hook-ds2zxq9v.js} +16 -18
- package/dist/{hook-pz50gqep.js → hook-vxa3tcsy.js} +10 -12
- package/dist/{http-ncr8rmd0.js → http-3h21z9md.js} +679 -227
- package/dist/{index-c4aqcyz1.js → index-9b65j1m5.js} +11 -11
- package/dist/{index-r46s01nq.js → index-bn6cy8ps.js} +22 -26
- package/dist/{index-38kqdt3e.js → index-dkfxpkmm.js} +23 -27
- package/dist/{index-630kj5vd.js → index-f8xnqrye.js} +55 -43
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-9nrj7797.js → index-n99fycq7.js} +22 -26
- package/dist/{keepalive-31613r2h.js → keepalive-3xspa9jw.js} +14 -11
- package/dist/{lead-mx59tr5s.js → lead-6qphc124.js} +43 -43
- package/dist/{maintenance-55ff39ca.js → maintenance-0teem1bb.js} +10 -12
- package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
- package/dist/{oauth-refresh-sweep-anma954w.js → oauth-refresh-sweep-sh9q25xp.js} +13 -10
- package/dist/{onboard-b2fy8ftb.js → onboard-54qmnqxe.js} +4 -3
- package/dist/{opencode-adapter-p1s92tmy.js → opencode-adapter-wjfa2wvd.js} +6 -6
- package/dist/{otel-impl-9brvt7xv.js → otel-impl-ycqzp2a1.js} +2 -2
- package/dist/{pi-mono-adapter-kz4rrwvd.js → pi-mono-adapter-e77e9zst.js} +6 -6
- package/dist/{pricing-refresh-7w8mbmva.js → pricing-refresh-atnkngqs.js} +10 -10
- package/dist/{rbac-roles-ejp7qwvb.js → rbac-roles-9pjphh9q.js} +10 -10
- package/dist/{rbac-roles-nhk4r7bc.js → rbac-roles-j78wdksm.js} +10 -10
- package/dist/{render-v2-2gzt8skq.js → render-v2-pxjs49fx.js} +9 -10
- package/dist/{setup-5et8t412.js → setup-ymej0xtx.js} +2 -2
- package/dist/{work-objects-tb0er7qa.js → work-objects-3nh4ymzb.js} +26 -30
- package/dist/{worker-62cv075d.js → worker-qa9fkz5s.js} +43 -43
- package/dist/{x-vbxb31jr.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +457 -31
- package/package.json +14 -6
- package/plugin/commands/create-pr.md +7 -1
- package/plugin/pi-skills/create-pr/SKILL.md +7 -1
- package/src/agentmail/app.ts +5 -4
- package/src/be/automation-preflight.ts +11 -9
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/connector-codes.ts +81 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/197_connector_codes.sql +23 -0
- package/src/be/migrations/198_permission_audit_guest.sql +33 -0
- package/src/be/migrations/200_backfill_auto_memory_expiry.sql +20 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/model-tier-resolution.ts +2 -1
- package/src/be/model-validation.ts +1 -1
- package/src/be/pricing-normalize.ts +3 -0
- package/src/be/rbac-audit.ts +2 -1
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scripts/typecheck.ts +23 -2
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/seed-pricing.ts +14 -0
- package/src/be/users.ts +7 -1
- package/src/be/workflow-replay.ts +77 -0
- package/src/cli.tsx +7 -0
- package/src/commands/provider-credentials.ts +46 -8
- package/src/commands/runner.ts +2 -10
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/github/app.ts +4 -4
- package/src/heartbeat/heartbeat.ts +20 -8
- package/src/hooks/pr-body-guard.ts +1 -1
- package/src/http/agents.ts +1 -0
- package/src/http/approval-requests.ts +1 -0
- package/src/http/apps.ts +3 -0
- package/src/http/auth.ts +4 -1
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/codex-oauth-keep-warm.ts +2 -2
- package/src/http/config-values.ts +59 -0
- package/src/http/config.ts +5 -0
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/extensions.ts +1 -0
- package/src/http/index.ts +23 -6
- package/src/http/integrations.ts +1 -38
- package/src/http/ip-rate-limit.ts +82 -0
- package/src/http/kv.ts +38 -4
- package/src/http/mcp-servers.ts +1 -0
- package/src/http/memory.ts +9 -0
- package/src/http/models-catalog.ts +2 -0
- package/src/http/operator-actor.ts +4 -0
- package/src/http/page-proxy.ts +22 -0
- package/src/http/pages.ts +4 -2
- package/src/http/realtime.ts +1 -1
- package/src/http/request-principal.ts +14 -1
- package/src/http/script-connections.ts +8 -0
- package/src/http/script-runs.ts +12 -4
- package/src/http/scripts.ts +15 -0
- package/src/http/session-cost-recompute.ts +3 -2
- package/src/http/tasks.ts +9 -0
- package/src/http/users.ts +226 -0
- package/src/http/workflows.ts +3 -2
- package/src/http.ts +1 -0
- package/src/integrations/kapso/client.ts +3 -1
- package/src/jira/app.ts +2 -2
- package/src/kv-view.ts +324 -0
- package/src/linear/app.ts +2 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/prompts/memories.ts +42 -1
- package/src/providers/acp-adapter.ts +85 -24
- package/src/providers/acp-target-catalog.ts +54 -1
- package/src/providers/acp-targets.ts +358 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/grok-adapter.ts +186 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/pricing-sources.md +6 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/rbac/can.ts +4 -3
- package/src/rbac/legacy-policy.ts +3 -3
- package/src/rbac/types.ts +6 -0
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +27 -8
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -8
- package/src/server-user.ts +2 -20
- package/src/server.ts +3 -1
- package/src/slack/app.ts +4 -0
- package/src/slack/config.ts +3 -1
- package/src/slack/egress-scrub.ts +87 -0
- package/src/tests/acp-adapter.test.ts +191 -2
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/budget-user-scope.test.ts +7 -21
- package/src/tests/check-pr-body.test.ts +149 -6
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/claude-adapter-binary.test.ts +1 -1
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/codex-oauth-keep-warm.test.ts +29 -0
- package/src/tests/console-scrub.test.ts +141 -0
- package/src/tests/credential-check.test.ts +90 -0
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/entrypoint-acp-target-binary.test.ts +44 -1
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/explicit-model-validation.test.ts +81 -0
- package/src/tests/extensions-pre-task.test.ts +33 -1
- package/src/tests/fixtures/grok/acp-updates-recorded.jsonl +53 -0
- package/src/tests/fixtures/grok/prompt-response-meta.json +36 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/grok-adapter.test.ts +647 -0
- package/src/tests/heartbeat-checklist.test.ts +44 -0
- package/src/tests/integration-disable-flags.test.ts +156 -0
- package/src/tests/kv-http.test.ts +34 -0
- package/src/tests/kv-tool.test.ts +274 -1
- package/src/tests/kv-view.test.ts +147 -0
- package/src/tests/mcp-user-route.test.ts +257 -15
- package/src/tests/memory-injection-outcome.test.ts +143 -0
- package/src/tests/model-tier-resolution.test.ts +76 -3
- package/src/tests/page-guest-session.test.ts +424 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-viewer-auth.test.ts +22 -1
- package/src/tests/pr-body-leaks.test.ts +157 -2
- package/src/tests/preload.ts +10 -0
- package/src/tests/provider-registration.test.ts +3 -1
- package/src/tests/rbac-audit.test.ts +32 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +4 -62
- package/src/tests/script-connections.test.ts +9 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +126 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scripts-mcp-e2e.test.ts +42 -6
- package/src/tests/scripts-runtime.test.ts +37 -0
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +317 -1
- package/src/tests/session-costs-golden.test.ts +17 -0
- package/src/tests/slack-egress-scrub.test.ts +216 -0
- package/src/tests/swarm-tool-result-gate.test.ts +143 -30
- package/src/tests/task-tools-ctx.test.ts +3 -0
- package/src/tests/user-token-routes.test.ts +342 -3
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +737 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/kv/kv-get.ts +136 -6
- package/src/tools/send-task.ts +61 -17
- package/src/tools/utils.ts +186 -43
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +16 -1
- package/src/utils/console-scrub-install.ts +5 -0
- package/src/utils/console-scrub.ts +57 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/page-session.ts +6 -2
- package/src/utils/pr-body-leaks.ts +149 -10
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +662 -36
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/executors/notify.ts +6 -1
- package/src/workflows/executors/system-one-decision.ts +31 -12
- package/src/workflows/executors/system-one-openai-wire.ts +168 -0
- package/src/workflows/executors/system-one-providers.ts +112 -11
- package/src/workflows/executors/system-one-wire.ts +79 -0
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/skills/agent-browser/SKILL.md +5 -5
- package/templates/skills/agent-browser/config.json +1 -1
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/artifacts/content.md +17 -7
- package/templates/skills/jira-interaction/config.json +1 -1
- package/templates/skills/jira-interaction/content.md +56 -205
- package/templates/skills/linear-interaction/config.json +1 -1
- package/templates/skills/linear-interaction/content.md +31 -50
- package/templates/skills/pages/content.md +8 -4
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/workflow-iterate/content.md +4 -4
- package/tsconfig.json +3 -3
- package/dist/boot-scrub-logs-t8xg7wd7.js +0 -99
- package/dist/cli-0a7r5331.js +0 -168
- package/dist/cli-93rxw9yd.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-dgnj0zcc.js +0 -40
- package/dist/cli-qt5wzq6v.js +0 -256
- package/dist/cli-v7fqehqd.js +0 -9281
- package/dist/cli-vbr859rz.js +0 -572
- package/src/be/boot-scrub-logs.ts +0 -132
package/src/http/users.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* PATCH /api/users/:id
|
|
19
19
|
* POST /api/users/:id/mcp-tokens
|
|
20
20
|
* DELETE /api/users/:id/mcp-tokens/:tokenId
|
|
21
|
+
* POST /api/users/:id/connector-codes
|
|
22
|
+
* POST /api/connector/exchange (public: the code is the credential)
|
|
21
23
|
* POST /api/users/:id/merge
|
|
22
24
|
* GET /api/users/:id/events
|
|
23
25
|
* POST /api/users/:id/identities
|
|
@@ -26,6 +28,11 @@
|
|
|
26
28
|
|
|
27
29
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
28
30
|
import { z } from "zod";
|
|
31
|
+
import {
|
|
32
|
+
createConnectorCode,
|
|
33
|
+
DEFAULT_CONNECTOR_CODE_LABEL,
|
|
34
|
+
exchangeConnectorCode,
|
|
35
|
+
} from "../be/connector-codes";
|
|
29
36
|
import {
|
|
30
37
|
createUser,
|
|
31
38
|
deleteBudget,
|
|
@@ -51,6 +58,8 @@ import {
|
|
|
51
58
|
} from "../be/users";
|
|
52
59
|
import { UserCommsPrefsSchema, UserSchema } from "../types";
|
|
53
60
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
61
|
+
import { resolveAppUrl, resolveMcpBaseUrl } from "./config-values";
|
|
62
|
+
import { clientIp, createIpRateLimiter } from "./ip-rate-limit";
|
|
54
63
|
import { getOperatorActor } from "./operator-actor";
|
|
55
64
|
import { route } from "./route-def";
|
|
56
65
|
import { jsonError } from "./utils";
|
|
@@ -378,6 +387,87 @@ const revokeUserMcpTokenRoute = route({
|
|
|
378
387
|
auth: { apiKey: true },
|
|
379
388
|
});
|
|
380
389
|
|
|
390
|
+
const createConnectorCodeRoute = route({
|
|
391
|
+
method: "post",
|
|
392
|
+
path: "/api/users/{id}/connector-codes",
|
|
393
|
+
pattern: ["api", "users", null, "connector-codes"],
|
|
394
|
+
summary: "Create a single-use connect code for the agent-swarm.dev ChatGPT connector",
|
|
395
|
+
description:
|
|
396
|
+
"Returns a 10-minute single-use code and the connector URL that carries it. The connector trades the code for an MCP token at POST /api/connector/exchange.",
|
|
397
|
+
tags: ["Users"],
|
|
398
|
+
params: z.object({ id: z.string() }),
|
|
399
|
+
body: z.object({
|
|
400
|
+
label: z.string().min(1).optional(),
|
|
401
|
+
}),
|
|
402
|
+
responses: {
|
|
403
|
+
201: {
|
|
404
|
+
description: "Connect code, its expiry and the connector URL",
|
|
405
|
+
schema: z.object({
|
|
406
|
+
code: z.string(),
|
|
407
|
+
expiresAt: z.string(),
|
|
408
|
+
connectUrl: z.string(),
|
|
409
|
+
}),
|
|
410
|
+
},
|
|
411
|
+
400: { description: "The public API origin or CONNECTOR_CONNECT_URL is not HTTPS" },
|
|
412
|
+
401: { description: "Unauthorized" },
|
|
413
|
+
404: { description: "User not found" },
|
|
414
|
+
},
|
|
415
|
+
auth: { apiKey: true },
|
|
416
|
+
rbac: { ungated: "same posture as POST /api/users/{id}/mcp-tokens (operator-only admission)" },
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const exchangeConnectorCodeRoute = route({
|
|
420
|
+
method: "post",
|
|
421
|
+
path: "/api/connector/exchange",
|
|
422
|
+
pattern: ["api", "connector", "exchange"],
|
|
423
|
+
summary: "Exchange a connect code for a user MCP token",
|
|
424
|
+
description:
|
|
425
|
+
"Unauthenticated: the single-use code is the credential. Rate limited per IP. Unknown, expired and used codes all return 404 code_invalid.",
|
|
426
|
+
tags: ["Users"],
|
|
427
|
+
body: z.object({ code: z.string().min(1).max(256) }),
|
|
428
|
+
// Public route: cap the body before it is buffered and parsed.
|
|
429
|
+
maxBodyBytes: 1024,
|
|
430
|
+
responses: {
|
|
431
|
+
200: {
|
|
432
|
+
description: "Minted MCP token for the code's user",
|
|
433
|
+
schema: z.object({
|
|
434
|
+
token: z.string(),
|
|
435
|
+
userId: z.string(),
|
|
436
|
+
version: z.string(),
|
|
437
|
+
}),
|
|
438
|
+
},
|
|
439
|
+
404: { description: "Code unknown, malformed, expired or already used" },
|
|
440
|
+
413: { description: "Request body too large" },
|
|
441
|
+
429: { description: "Rate limited" },
|
|
442
|
+
},
|
|
443
|
+
auth: { apiKey: false },
|
|
444
|
+
rbac: {
|
|
445
|
+
ungated:
|
|
446
|
+
"the single-use code is the credential; the minted token carries the user's own grant and is admitted on use",
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const connectorDiscoveryRoute = route({
|
|
451
|
+
method: "get",
|
|
452
|
+
path: "/api/connector/discovery",
|
|
453
|
+
pattern: ["api", "connector", "discovery"],
|
|
454
|
+
summary: "Public origins the agent-swarm.dev connector needs to find this swarm's dashboard",
|
|
455
|
+
description:
|
|
456
|
+
"Unauthenticated. Lets the connector turn an API origin into the dashboard URL that serves /connect. appUrl is omitted when APP_URL is not set or is not https.",
|
|
457
|
+
tags: ["Users"],
|
|
458
|
+
responses: {
|
|
459
|
+
200: {
|
|
460
|
+
description: "Public API origin, dashboard origin and connector connect URL",
|
|
461
|
+
schema: z.object({
|
|
462
|
+
apiUrl: z.string(),
|
|
463
|
+
appUrl: z.string().optional(),
|
|
464
|
+
connectUrl: z.string().nullable(),
|
|
465
|
+
}),
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
auth: { apiKey: false },
|
|
469
|
+
});
|
|
470
|
+
|
|
381
471
|
const mergeUsersRoute = route({
|
|
382
472
|
method: "post",
|
|
383
473
|
path: "/api/users/{id}/merge",
|
|
@@ -529,6 +619,54 @@ async function collectUnmappedForKind(kind: string, limit: number) {
|
|
|
529
619
|
return Array.from(byId.values());
|
|
530
620
|
}
|
|
531
621
|
|
|
622
|
+
const DEFAULT_CONNECTOR_CONNECT_URL = "https://mcp.agent-swarm.dev/connections";
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* The connector's connect page. Returns null unless it parses as an https://
|
|
626
|
+
* URL: the link carries a bearer-equivalent code, so it must never go out
|
|
627
|
+
* over plaintext or to a non-web scheme.
|
|
628
|
+
*/
|
|
629
|
+
function getConnectorConnectUrl(): URL | null {
|
|
630
|
+
const raw = process.env.CONNECTOR_CONNECT_URL?.trim() || DEFAULT_CONNECTOR_CONNECT_URL;
|
|
631
|
+
try {
|
|
632
|
+
const url = new URL(raw);
|
|
633
|
+
return url.protocol === "https:" ? url : null;
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* The connector only links https swarms in production. A loopback http
|
|
643
|
+
* origin also passes, for local stacks: there the connector's own SSRF guard
|
|
644
|
+
* (private swarms allowed only in its local env) makes the final call.
|
|
645
|
+
*/
|
|
646
|
+
function isConnectableSwarmOrigin(value: string): boolean {
|
|
647
|
+
try {
|
|
648
|
+
const url = new URL(value);
|
|
649
|
+
return (
|
|
650
|
+
url.protocol === "https:" || (url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname))
|
|
651
|
+
);
|
|
652
|
+
} catch {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** `createConnectorCode` output: 32 random bytes in unpadded base64url. */
|
|
658
|
+
const CONNECTOR_CODE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
659
|
+
|
|
660
|
+
/** 10 exchanges per minute per client IP. */
|
|
661
|
+
const connectorExchangeRateLimiter = createIpRateLimiter({
|
|
662
|
+
capacity: 10,
|
|
663
|
+
refillPerMs: 10 / 60_000,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
export function _resetConnectorExchangeRateLimitForTests(): void {
|
|
667
|
+
connectorExchangeRateLimiter.reset();
|
|
668
|
+
}
|
|
669
|
+
|
|
532
670
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
533
671
|
|
|
534
672
|
export async function handleUsers(
|
|
@@ -694,6 +832,94 @@ export async function handleUsers(
|
|
|
694
832
|
return true;
|
|
695
833
|
}
|
|
696
834
|
|
|
835
|
+
// ─── POST /api/users/:id/connector-codes ──────────────────────────────────
|
|
836
|
+
if (createConnectorCodeRoute.match(req.method, pathSegments)) {
|
|
837
|
+
const parsed = await createConnectorCodeRoute.parse(req, res, pathSegments, queryParams);
|
|
838
|
+
if (!parsed) return true;
|
|
839
|
+
const actor = getOperatorActor(req, res);
|
|
840
|
+
if (!actor) return true;
|
|
841
|
+
if (!(await getUserById(parsed.params.id))) {
|
|
842
|
+
jsonError(res, "User not found", 404);
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
// The connector refuses non-HTTPS swarm origins, so fail before minting a code.
|
|
846
|
+
// Same swarm_config-aware resolver as the dashboard's HTTPS gate.
|
|
847
|
+
const swarmOrigin = await resolveMcpBaseUrl();
|
|
848
|
+
if (!isConnectableSwarmOrigin(swarmOrigin)) {
|
|
849
|
+
jsonError(
|
|
850
|
+
res,
|
|
851
|
+
`The public API origin (${swarmOrigin}) is not HTTPS. Set PUBLIC_MCP_BASE_URL to an https:// origin.`,
|
|
852
|
+
400,
|
|
853
|
+
);
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
const connect = getConnectorConnectUrl();
|
|
857
|
+
if (!connect) {
|
|
858
|
+
jsonError(res, "CONNECTOR_CONNECT_URL must be an https:// URL.", 400);
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
const { code, expiresAt } = await createConnectorCode(
|
|
864
|
+
parsed.params.id,
|
|
865
|
+
parsed.body.label ?? DEFAULT_CONNECTOR_CODE_LABEL,
|
|
866
|
+
actor,
|
|
867
|
+
);
|
|
868
|
+
connect.searchParams.set("swarm", swarmOrigin);
|
|
869
|
+
connect.searchParams.set("code", code);
|
|
870
|
+
const connectUrl = connect.toString();
|
|
871
|
+
createConnectorCodeRoute.respond(res, 201, { code, expiresAt, connectUrl });
|
|
872
|
+
} catch (err) {
|
|
873
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to create connector code", 500);
|
|
874
|
+
}
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// ─── GET /api/connector/discovery ─────────────────────────────────────────
|
|
879
|
+
if (connectorDiscoveryRoute.match(req.method, pathSegments)) {
|
|
880
|
+
const parsed = await connectorDiscoveryRoute.parse(req, res, pathSegments, queryParams);
|
|
881
|
+
if (!parsed) return true;
|
|
882
|
+
// The connector only opens an https dashboard, so an http APP_URL is not reported.
|
|
883
|
+
const resolvedAppUrl = await resolveAppUrl();
|
|
884
|
+
const appUrl = resolvedAppUrl?.startsWith("https://") ? resolvedAppUrl : null;
|
|
885
|
+
connectorDiscoveryRoute.respond(res, 200, {
|
|
886
|
+
apiUrl: await resolveMcpBaseUrl(),
|
|
887
|
+
...(appUrl ? { appUrl } : {}),
|
|
888
|
+
connectUrl: getConnectorConnectUrl()?.toString() ?? null,
|
|
889
|
+
});
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ─── POST /api/connector/exchange ─────────────────────────────────────────
|
|
894
|
+
if (exchangeConnectorCodeRoute.match(req.method, pathSegments)) {
|
|
895
|
+
if (!connectorExchangeRateLimiter.take(clientIp(req))) {
|
|
896
|
+
res.setHeader("Retry-After", "6");
|
|
897
|
+
jsonError(res, "rate_limited", 429);
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
const parsed = await exchangeConnectorCodeRoute.parse(req, res, pathSegments, queryParams);
|
|
901
|
+
if (!parsed) return true;
|
|
902
|
+
|
|
903
|
+
// Reject anything that is not a generated code shape before hashing it.
|
|
904
|
+
if (!CONNECTOR_CODE_PATTERN.test(parsed.body.code)) {
|
|
905
|
+
jsonError(res, "code_invalid", 404);
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
const result = await exchangeConnectorCode(parsed.body.code);
|
|
911
|
+
if (!result) {
|
|
912
|
+
jsonError(res, "code_invalid", 404);
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
const version = (await Bun.file("package.json").json()).version;
|
|
916
|
+
exchangeConnectorCodeRoute.respond(res, 200, { ...result, version });
|
|
917
|
+
} catch (err) {
|
|
918
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to exchange code", 500);
|
|
919
|
+
}
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
697
923
|
// ─── DELETE /api/users/:id/mcp-tokens/:tokenId ────────────────────────────
|
|
698
924
|
if (revokeUserMcpTokenRoute.match(req.method, pathSegments)) {
|
|
699
925
|
const parsed = await revokeUserMcpTokenRoute.parse(req, res, pathSegments, queryParams);
|
package/src/http/workflows.ts
CHANGED
|
@@ -901,13 +901,14 @@ export async function handleWorkflows(
|
|
|
901
901
|
if (getWorkflowRunRoute.match(req.method, pathSegments)) {
|
|
902
902
|
const parsed = await getWorkflowRunRoute.parse(req, res, pathSegments, queryParams);
|
|
903
903
|
if (!parsed) return true;
|
|
904
|
-
|
|
904
|
+
// Display view: the scrubbed context and step outputs, never replay state.
|
|
905
|
+
const run = await getWorkflowRun(parsed.params.id, "display");
|
|
905
906
|
if (!run) {
|
|
906
907
|
res.writeHead(404);
|
|
907
908
|
res.end();
|
|
908
909
|
return true;
|
|
909
910
|
}
|
|
910
|
-
const steps = await getWorkflowRunStepsByRunId(parsed.params.id);
|
|
911
|
+
const steps = await getWorkflowRunStepsByRunId(parsed.params.id, "display");
|
|
911
912
|
getWorkflowRunRoute.respond(res, 200, { run, steps });
|
|
912
913
|
return true;
|
|
913
914
|
}
|
package/src/http.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* Both authenticate with the `X-API-Key` header.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { scrubSecrets } from "../../utils/secret-scrubber";
|
|
12
|
+
|
|
11
13
|
/** Result of an outbound text/reply send through the Meta proxy. */
|
|
12
14
|
export interface KapsoSendResult {
|
|
13
15
|
ok: boolean;
|
|
@@ -75,7 +77,7 @@ export async function sendKapsoText(params: {
|
|
|
75
77
|
recipient_type: "individual",
|
|
76
78
|
to: params.to,
|
|
77
79
|
type: "text",
|
|
78
|
-
text: { preview_url: params.previewUrl ?? false, body: params.body },
|
|
80
|
+
text: { preview_url: params.previewUrl ?? false, body: scrubSecrets(params.body) },
|
|
79
81
|
};
|
|
80
82
|
if (params.contextMessageId) {
|
|
81
83
|
payload.context = { message_id: params.contextMessageId };
|
package/src/jira/app.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { upsertOAuthApp } from "../be/db-queries/oauth";
|
|
2
2
|
import { getPublicMcpBaseUrl } from "../utils/constants";
|
|
3
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
3
4
|
import { initJiraOutboundSync, teardownJiraOutboundSync } from "./outbound";
|
|
4
5
|
// Side-effect import: registers all Jira event templates in the in-memory
|
|
5
6
|
// registry at module load time (mirrors `src/linear/templates.ts`).
|
|
@@ -10,8 +11,7 @@ import { startJiraWebhookKeepalive, stopJiraWebhookKeepalive } from "./webhook-l
|
|
|
10
11
|
let initialized = false;
|
|
11
12
|
|
|
12
13
|
export function isJiraEnabled(): boolean {
|
|
13
|
-
|
|
14
|
-
if (disabled === "true" || disabled === "1") return false;
|
|
14
|
+
if (isEnvFlagEnabled("JIRA_DISABLE", false)) return false;
|
|
15
15
|
const enabled = process.env.JIRA_ENABLED;
|
|
16
16
|
if (enabled === "false" || enabled === "0") return false;
|
|
17
17
|
return !!process.env.JIRA_CLIENT_ID;
|
package/src/kv-view.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Targeted reads of a stored KV value: a dot path into a JSON value plus an
|
|
3
|
+
* offset/limit window over the array items, object keys, or string characters
|
|
4
|
+
* found there. Shared by the kv-get MCP tool, the REST GET routes behind
|
|
5
|
+
* `ctx.swarm.kv_get`, and the ctx-control spill shape summary, so every path
|
|
6
|
+
* the summary prints is a path kv-get accepts.
|
|
7
|
+
*
|
|
8
|
+
* Pure functions only: no DB access, safe to import from any side.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type KvViewArgs = { path?: string; offset?: number; limit?: number };
|
|
12
|
+
|
|
13
|
+
export type KvViewType = "object" | "array" | "string" | "number" | "boolean" | "null";
|
|
14
|
+
|
|
15
|
+
export type KvView = {
|
|
16
|
+
/** Dot path that was resolved; "" is the whole value. */
|
|
17
|
+
path: string;
|
|
18
|
+
type: KvViewType;
|
|
19
|
+
/** Array items, object keys, or string characters at `path`. */
|
|
20
|
+
total?: number;
|
|
21
|
+
offset?: number;
|
|
22
|
+
returned?: number;
|
|
23
|
+
/** Offset of the next page, or null when this page reaches the end. */
|
|
24
|
+
nextOffset?: number | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type KvViewResult =
|
|
28
|
+
| { ok: true; value: unknown; view: KvView }
|
|
29
|
+
| { ok: false; error: string };
|
|
30
|
+
|
|
31
|
+
/** Longest `path` kv-get accepts. */
|
|
32
|
+
export const KV_PATH_MAX_CHARS = 1024;
|
|
33
|
+
|
|
34
|
+
/** JSON-encoded path bytes a spill shape summary may spend across its entries. */
|
|
35
|
+
export const KV_SHAPE_PATH_BUDGET_BYTES = 2048;
|
|
36
|
+
|
|
37
|
+
function pathBytes(path: string): number {
|
|
38
|
+
return Buffer.byteLength(JSON.stringify(path), "utf8");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type KvShapeEntry = {
|
|
42
|
+
path: string;
|
|
43
|
+
type: KvViewType;
|
|
44
|
+
bytes: number;
|
|
45
|
+
/** Array items, object keys, or string characters. */
|
|
46
|
+
items?: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function hasKvViewArgs(args: KvViewArgs): boolean {
|
|
50
|
+
return args.path !== undefined || args.offset !== undefined || args.limit !== undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function kvViewType(value: unknown): KvViewType {
|
|
54
|
+
if (value === null || value === undefined) return "null";
|
|
55
|
+
if (Array.isArray(value)) return "array";
|
|
56
|
+
switch (typeof value) {
|
|
57
|
+
case "object":
|
|
58
|
+
return "object";
|
|
59
|
+
case "string":
|
|
60
|
+
return "string";
|
|
61
|
+
case "number":
|
|
62
|
+
case "bigint":
|
|
63
|
+
return "number";
|
|
64
|
+
case "boolean":
|
|
65
|
+
return "boolean";
|
|
66
|
+
default:
|
|
67
|
+
return "null";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The JSON root of a stored value. `string` entries whose text parses to an
|
|
73
|
+
* object or array (MCP spill payloads are stored this way) are treated as
|
|
74
|
+
* JSON; any other string stays a plain string.
|
|
75
|
+
*/
|
|
76
|
+
export function kvViewRoot(value: unknown): { root: unknown; json: boolean } {
|
|
77
|
+
if (typeof value !== "string") return { root: value, json: true };
|
|
78
|
+
try {
|
|
79
|
+
const parsed: unknown = JSON.parse(value);
|
|
80
|
+
if (parsed !== null && typeof parsed === "object") return { root: parsed, json: true };
|
|
81
|
+
} catch {
|
|
82
|
+
// Not JSON: fall through to plain-string handling.
|
|
83
|
+
}
|
|
84
|
+
return { root: value, json: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function itemCount(value: unknown): number | undefined {
|
|
88
|
+
if (Array.isArray(value)) return value.length;
|
|
89
|
+
if (typeof value === "string") return value.length;
|
|
90
|
+
if (value !== null && typeof value === "object") return Object.keys(value).length;
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Split a dot path into keys. `\.` is a literal dot and `\\` a literal
|
|
96
|
+
* backslash, so object keys containing dots stay addressable.
|
|
97
|
+
*/
|
|
98
|
+
export function splitKvPath(path: string): string[] {
|
|
99
|
+
const segments: string[] = [];
|
|
100
|
+
let current = "";
|
|
101
|
+
for (let i = 0; i < path.length; i++) {
|
|
102
|
+
const char = path[i] as string;
|
|
103
|
+
if (char === "\\" && i + 1 < path.length) {
|
|
104
|
+
current += path[++i];
|
|
105
|
+
} else if (char === ".") {
|
|
106
|
+
segments.push(current);
|
|
107
|
+
current = "";
|
|
108
|
+
} else {
|
|
109
|
+
current += char;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
segments.push(current);
|
|
113
|
+
return segments;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Inverse of `splitKvPath`: escape each key and join with dots. */
|
|
117
|
+
export function joinKvPath(segments: string[]): string {
|
|
118
|
+
return segments.map((key) => key.replace(/[\\.]/g, "\\$&")).join(".");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolvePath(
|
|
122
|
+
root: unknown,
|
|
123
|
+
path: string,
|
|
124
|
+
): { ok: true; value: unknown } | { ok: false; error: string } {
|
|
125
|
+
if (path === "") return { ok: true, value: root };
|
|
126
|
+
const segments = splitKvPath(path);
|
|
127
|
+
if (segments.some((segment) => segment === "")) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: `path "${path}" has an empty segment; use dot paths like "data.rows"`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
let current = root;
|
|
134
|
+
for (let i = 0; i < segments.length; i++) {
|
|
135
|
+
const segment = segments[i] as string;
|
|
136
|
+
const at = joinKvPath(segments.slice(0, i)) || "(root)";
|
|
137
|
+
if (Array.isArray(current)) {
|
|
138
|
+
const index = /^\d+$/.test(segment) ? Number(segment) : Number.NaN;
|
|
139
|
+
if (!(index < current.length)) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: `path "${path}" not found: "${at}" is an array of ${current.length}; "${segment}" is not a valid index`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
current = current[index];
|
|
146
|
+
} else if (current !== null && typeof current === "object") {
|
|
147
|
+
if (!Object.hasOwn(current, segment)) {
|
|
148
|
+
return { ok: false, error: `path "${path}" not found: no key "${segment}" under "${at}"` };
|
|
149
|
+
}
|
|
150
|
+
current = (current as Record<string, unknown>)[segment];
|
|
151
|
+
} else {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: `path "${path}" not found: "${at}" is a ${kvViewType(current)}, it has no "${segment}"`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, value: current };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The value at `path` ("" = whole value) inside a stored KV value. */
|
|
162
|
+
export function resolveKvTarget(
|
|
163
|
+
value: unknown,
|
|
164
|
+
path: string,
|
|
165
|
+
): { ok: true; value: unknown } | { ok: false; error: string } {
|
|
166
|
+
const { root, json } = kvViewRoot(value);
|
|
167
|
+
if (path !== "" && !json) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
error: `path "${path}" needs a JSON value; this entry is a plain string (use offset/limit alone to page its characters)`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return resolvePath(root, path);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Window `[offset, offset + limit)` over a target's items, keys, or characters. */
|
|
177
|
+
export function sliceKvTarget(
|
|
178
|
+
target: unknown,
|
|
179
|
+
offset: number,
|
|
180
|
+
limit: number | undefined,
|
|
181
|
+
): { value: unknown; total: number; returned: number; nextOffset: number | null } | undefined {
|
|
182
|
+
const total = itemCount(target);
|
|
183
|
+
if (total === undefined) return undefined;
|
|
184
|
+
const start = Math.min(offset, total);
|
|
185
|
+
const end = limit === undefined ? total : Math.min(total, start + limit);
|
|
186
|
+
let value: unknown;
|
|
187
|
+
let returned = end - start;
|
|
188
|
+
if (Array.isArray(target)) {
|
|
189
|
+
value = target.slice(start, end);
|
|
190
|
+
} else if (typeof target === "string") {
|
|
191
|
+
// Never end a page on a lone high surrogate: back off one char so the next
|
|
192
|
+
// page starts on the pair, or take the whole pair when it is the only char
|
|
193
|
+
// (otherwise `limit: 1` would return nothing and never advance).
|
|
194
|
+
const lastCode = target.charCodeAt(end - 1);
|
|
195
|
+
const splitsPair = end > start && end < total && lastCode >= 0xd800 && lastCode <= 0xdbff;
|
|
196
|
+
const safeEnd = splitsPair ? (end - 1 > start ? end - 1 : end + 1) : end;
|
|
197
|
+
value = target.slice(start, safeEnd);
|
|
198
|
+
returned = safeEnd - start;
|
|
199
|
+
} else {
|
|
200
|
+
value = Object.fromEntries(Object.entries(target as object).slice(start, end));
|
|
201
|
+
}
|
|
202
|
+
const reached = start + returned;
|
|
203
|
+
return { value, total, returned, nextOffset: reached < total ? reached : null };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resolve `path`, then apply `offset`/`limit`. Out-of-range offsets return an
|
|
208
|
+
* empty page with the true `total`; missing paths and paths into a plain
|
|
209
|
+
* string are errors. No size cap here: callers that feed a model bound the
|
|
210
|
+
* page themselves (see the kv-get tool).
|
|
211
|
+
*/
|
|
212
|
+
export function resolveKvView(value: unknown, args: KvViewArgs): KvViewResult {
|
|
213
|
+
const path = args.path ?? "";
|
|
214
|
+
const resolved = resolveKvTarget(value, path);
|
|
215
|
+
if (!resolved.ok) return resolved;
|
|
216
|
+
const type = kvViewType(resolved.value);
|
|
217
|
+
const offset = args.offset ?? 0;
|
|
218
|
+
const sliced = sliceKvTarget(resolved.value, offset, args.limit);
|
|
219
|
+
if (!sliced) {
|
|
220
|
+
if (args.offset !== undefined || args.limit !== undefined) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
error: `offset/limit page arrays, objects, and strings; "${path || "(root)"}" is a ${type}`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { ok: true, value: resolved.value, view: { path, type } };
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
value: sliced.value,
|
|
231
|
+
view: {
|
|
232
|
+
path,
|
|
233
|
+
type,
|
|
234
|
+
total: sliced.total,
|
|
235
|
+
offset,
|
|
236
|
+
returned: sliced.returned,
|
|
237
|
+
nextOffset: sliced.nextOffset,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
type ShapeNode = KvShapeEntry & { value: unknown; segments: string[]; ancestors: unknown[] };
|
|
243
|
+
|
|
244
|
+
function shapeEntry(segments: string[], value: unknown, ancestors: unknown[]): ShapeNode {
|
|
245
|
+
const items = itemCount(value);
|
|
246
|
+
return {
|
|
247
|
+
path: joinKvPath(segments),
|
|
248
|
+
type: kvViewType(value),
|
|
249
|
+
bytes: Buffer.byteLength(JSON.stringify(value) ?? "", "utf8"),
|
|
250
|
+
...(items !== undefined ? { items } : {}),
|
|
251
|
+
value,
|
|
252
|
+
segments,
|
|
253
|
+
ancestors,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function childEntries(node: Pick<ShapeNode, "value" | "segments" | "ancestors">): ShapeNode[] {
|
|
258
|
+
const { value, segments, ancestors } = node;
|
|
259
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return [];
|
|
260
|
+
return Object.entries(value).map(([key, child]) =>
|
|
261
|
+
shapeEntry([...segments, key], child, [...ancestors, value]),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The node itself when kv-get can address it within `budget` (JSON-encoded
|
|
267
|
+
* path bytes, what a path costs on the wire), else its nearest such ancestor.
|
|
268
|
+
* An empty key has no path syntax (`""` is the root and empty segments are
|
|
269
|
+
* rejected), so its parent stands in; the root always fits.
|
|
270
|
+
*/
|
|
271
|
+
function addressable(node: ShapeNode, budget: number): ShapeNode {
|
|
272
|
+
for (let depth = node.segments.length; depth > 0; depth--) {
|
|
273
|
+
const segments = node.segments.slice(0, depth);
|
|
274
|
+
if (segments.includes("")) continue;
|
|
275
|
+
const path = joinKvPath(segments);
|
|
276
|
+
if (path.length > KV_PATH_MAX_CHARS || pathBytes(path) > budget) continue;
|
|
277
|
+
if (depth === node.segments.length) return node;
|
|
278
|
+
return shapeEntry(segments, node.ancestors[depth], node.ancestors.slice(0, depth));
|
|
279
|
+
}
|
|
280
|
+
return shapeEntry([], node.ancestors[0] ?? node.value, []);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Bounded outline of a JSON value: its biggest branches by serialized bytes,
|
|
285
|
+
* expanding the dominant object one level at a time so the paths reach where
|
|
286
|
+
* the bulk lives (e.g. `outcome.data.data.result.rows`). Arrays are reported
|
|
287
|
+
* with their length and never expanded.
|
|
288
|
+
*/
|
|
289
|
+
export function summarizeKvShape(
|
|
290
|
+
root: unknown,
|
|
291
|
+
maxEntries = 8,
|
|
292
|
+
maxPathBytes = KV_SHAPE_PATH_BUDGET_BYTES,
|
|
293
|
+
): KvShapeEntry[] {
|
|
294
|
+
const rootBytes = Buffer.byteLength(JSON.stringify(root) ?? "", "utf8");
|
|
295
|
+
let entries = childEntries({ value: root, segments: [], ancestors: [] });
|
|
296
|
+
for (let step = 0; step < 8; step++) {
|
|
297
|
+
const candidate = entries
|
|
298
|
+
.filter((entry) => entry.type === "object" && (entry.items ?? 0) > 0)
|
|
299
|
+
.sort((a, b) => b.bytes - a.bytes)[0];
|
|
300
|
+
if (!candidate || candidate.bytes * 4 < rootBytes) break;
|
|
301
|
+
entries = entries.filter((entry) => entry !== candidate).concat(childEntries(candidate));
|
|
302
|
+
}
|
|
303
|
+
// Skip crumbs (under 1% of the value) so the slots go to where the bulk is.
|
|
304
|
+
const sorted = entries.sort((a, b) => b.bytes - a.bytes);
|
|
305
|
+
const significant = sorted.filter((entry) => entry.bytes * 100 >= rootBytes);
|
|
306
|
+
// Paths stay exact so each one works as a kv-get `path`. The summary is
|
|
307
|
+
// bounded by total path bytes: a path that does not fit the remaining budget
|
|
308
|
+
// gives way to its nearest ancestor that does.
|
|
309
|
+
const shape: KvShapeEntry[] = [];
|
|
310
|
+
let budget = maxPathBytes;
|
|
311
|
+
for (const entry of significant.length > 0 ? significant : sorted) {
|
|
312
|
+
if (shape.length >= maxEntries) break;
|
|
313
|
+
const {
|
|
314
|
+
value: _value,
|
|
315
|
+
segments: _segments,
|
|
316
|
+
ancestors: _ancestors,
|
|
317
|
+
...node
|
|
318
|
+
} = addressable(entry, budget);
|
|
319
|
+
if (shape.some((seen) => seen.path === node.path)) continue;
|
|
320
|
+
shape.push(node);
|
|
321
|
+
budget -= pathBytes(node.path);
|
|
322
|
+
}
|
|
323
|
+
return shape;
|
|
324
|
+
}
|
package/src/linear/app.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getOAuthApp, upsertOAuthApp } from "../be/db-queries/oauth";
|
|
2
2
|
import { onAuthorizationRefreshed } from "../oauth/ensure-token";
|
|
3
3
|
import { getPublicMcpBaseUrl } from "../utils/constants";
|
|
4
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
4
5
|
import { resetLinearClient } from "./client";
|
|
5
6
|
import { initLinearOutboundSync, teardownLinearOutboundSync } from "./outbound";
|
|
6
7
|
|
|
@@ -8,8 +9,7 @@ let initialized = false;
|
|
|
8
9
|
let unsubscribeRefresh: (() => void) | null = null;
|
|
9
10
|
|
|
10
11
|
export function isLinearEnabled(): boolean {
|
|
11
|
-
|
|
12
|
-
if (disabled === "true" || disabled === "1") return false;
|
|
12
|
+
if (isEnvFlagEnabled("LINEAR_DISABLE", false)) return false;
|
|
13
13
|
const enabled = process.env.LINEAR_ENABLED;
|
|
14
14
|
if (enabled === "false" || enabled === "0") return false;
|
|
15
15
|
return !!process.env.LINEAR_CLIENT_ID;
|
package/src/oauth/mcp-wrapper.ts
CHANGED
|
@@ -372,7 +372,7 @@ function formUrlEncodeComponent(value: string): string {
|
|
|
372
372
|
* at each call site means a new sensitive field cannot forget one shape.
|
|
373
373
|
*/
|
|
374
374
|
function redactSentCredentials(text: string, sent: Array<string | null | undefined>): string {
|
|
375
|
-
let out = scrubSecrets(text);
|
|
375
|
+
let out: string = scrubSecrets(text);
|
|
376
376
|
for (const value of sent) {
|
|
377
377
|
if (!value) continue;
|
|
378
378
|
for (const shape of new Set([value, formUrlEncodeComponent(value)])) {
|