@desplega.ai/agent-swarm 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{acp-adapter-kedstxx6.js → acp-adapter-dv7hmx4r.js} +9 -9
- package/dist/{actions-vget6r1k.js → actions-51nc1gny.js} +21 -21
- package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-abp5tdx8.js} +4 -4
- package/dist/{app-mqap8t8m.js → app-9h14qvr6.js} +3 -3
- package/dist/{assistant-h0gv97mv.js → assistant-wv1gz3mt.js} +22 -22
- package/dist/{boot-reembed-qr8a894z.js → boot-reembed-6yn53ms9.js} +1 -1
- package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-7fcka4z5.js} +1 -1
- package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-pm1580ax.js} +4 -4
- package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-g5hfc1x8.js} +4 -4
- package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gf3kxhf0.js} +9 -9
- package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
- package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
- package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-s2k1qd8y.js} +13 -13
- package/dist/{cli-tj4j2ts4.js → cli-02qm7src.js} +8 -8
- package/dist/{cli-1cevz46c.js → cli-0qvtffe9.js} +6 -6
- package/dist/{cli-avmqhatn.js → cli-10hhg0km.js} +8 -8
- package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
- package/dist/{cli-kz116kvw.js → cli-239yjk27.js} +7 -7
- package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
- package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
- package/dist/{cli-x719j3vs.js → cli-5rk36sjf.js} +442 -205
- package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
- package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
- package/dist/{cli-5s5hx57y.js → cli-7awf1fp8.js} +1 -1
- package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
- package/dist/{cli-ywyrffff.js → cli-8sxqsbbq.js} +1 -1
- package/dist/{cli-rsd7ga7v.js → cli-8zwe9mpk.js} +11 -11
- package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
- package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
- package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
- package/dist/{cli-mvg2hf8p.js → cli-ahtpy9kf.js} +14 -7
- package/dist/{cli-x1p5dhq2.js → cli-bcj753sq.js} +3 -3
- package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
- package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
- package/dist/{cli-rg7ntypm.js → cli-d3qqdy4c.js} +456 -94
- package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
- package/dist/{cli-02vhhssk.js → cli-efaqv1c7.js} +5 -5
- package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
- package/dist/{cli-kk2r80n5.js → cli-faypxz7q.js} +4 -4
- package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
- package/dist/{cli-wrv09v7q.js → cli-ftyypbg5.js} +1 -1
- package/dist/{cli-zvze9rbm.js → cli-fyzjmseb.js} +3 -3
- package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
- package/dist/{cli-nrrg0fvy.js → cli-h38rjv87.js} +133 -12
- package/dist/{cli-tg9ksbn8.js → cli-h93c9vb3.js} +2 -2
- package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
- package/dist/{cli-bhwa8rzc.js → cli-hyfw2j1e.js} +13 -12
- package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
- package/dist/{cli-6ftrrwm1.js → cli-n85dqg6d.js} +5 -5
- package/dist/{cli-8cxkw1f5.js → cli-nmqdc2t3.js} +2 -2
- package/dist/{cli-4fhyq711.js → cli-nn47d7e8.js} +13 -13
- package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
- package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
- package/dist/{cli-xfe63zx3.js → cli-pkht5hap.js} +2 -2
- package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
- package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
- package/dist/{cli-tgvpmnwz.js → cli-r99kc9yq.js} +5 -5
- package/dist/{cli-bfc3k1x5.js → cli-rdyw3mct.js} +9 -9
- package/dist/{cli-b80a1100.js → cli-s6vr7r2d.js} +2 -2
- package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
- package/dist/{cli-ttaz6c7m.js → cli-saw9a6f6.js} +6 -5
- package/dist/{cli-mz1ezzzh.js → cli-shvfedvm.js} +3 -3
- package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
- package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
- package/dist/{cli-5ff2sjyt.js → cli-t30yj95q.js} +2 -2
- package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
- package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
- package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
- package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
- package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
- package/dist/{cli-nme7wery.js → cli-y2x52gs2.js} +46 -13
- package/dist/{cli-ffbrkh1r.js → cli-y8n5k515.js} +46 -38
- package/dist/{cli-ecshf504.js → cli-z4wd2ftv.js} +7 -7
- package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
- package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
- package/dist/cli.js +20 -20
- package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-ttygw382.js} +6 -6
- package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
- package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
- package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-60b58vv3.js} +7 -7
- package/dist/{commands-psqhkttb.js → commands-edmdwa4x.js} +2 -2
- package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
- package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-5fym8sw1.js} +23 -23
- package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
- package/dist/{db-nc1rd869.js → db-2y0mgstc.js} +3 -3
- package/dist/{db-2xbyenjr.js → db-nn0hcmx5.js} +6 -2
- package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
- package/dist/{dispatcher-ewceb8ts.js → dispatcher-0a9wwjcx.js} +20 -20
- package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
- package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
- package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
- package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
- package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
- package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
- package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
- package/dist/{external-channel-a5dsmxn4.js → external-channel-zhvt95n3.js} +2 -2
- package/dist/{grok-adapter-vj88xr23.js → grok-adapter-qnr1hp2y.js} +10 -10
- package/dist/handlers-dkcarkd0.js +39 -0
- package/dist/{heartbeat-t2m9r3wa.js → heartbeat-gz97r7ac.js} +23 -23
- package/dist/{hook-xa677f8r.js → hook-1mmdgjqs.js} +16 -9
- package/dist/{hook-8femx1bg.js → hook-x8rthca4.js} +6 -6
- package/dist/{http-xpmmq028.js → http-r8f85xr9.js} +274 -160
- package/dist/index-5jecgp3n.js +41 -0
- package/dist/index-aex0eba9.js +40 -0
- package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
- package/dist/{index-cdzyfrsp.js → index-smxyk04s.js} +1 -1
- package/dist/{index-dg2tmj99.js → index-vzvfqzfj.js} +20 -20
- package/dist/{index-s7ckdajq.js → index-y7rbcrc1.js} +10 -10
- package/dist/{keepalive-3nr4st3r.js → keepalive-64c0f34c.js} +4 -4
- package/dist/lead-b1da0f97.js +78 -0
- package/dist/{maintenance-verwpe2c.js → maintenance-mnxpar12.js} +1 -1
- package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-36hs2v4h.js} +3 -3
- package/dist/{onboard-aytqedmq.js → onboard-xes8de61.js} +3 -3
- package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
- package/dist/{otel-impl-hckw4wt1.js → otel-impl-fzc57kcq.js} +2 -2
- package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
- package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-1nek2w7b.js} +3 -3
- package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-sqefb5hw.js} +2 -2
- package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-ttyfaep2.js} +1 -1
- package/dist/{render-v2-7nqgvk2q.js → render-v2-vb527jwn.js} +3 -3
- package/dist/{resume-b6djcj3c.js → resume-jcdzg1bn.js} +10 -8
- package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
- package/dist/{session-access-fc3f9y12.js → session-access-zsp4wsp0.js} +4 -4
- package/dist/{setup-n39szm6b.js → setup-1nb2zqd3.js} +1 -1
- package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
- package/dist/{work-objects-jem2fre0.js → work-objects-61w08fwr.js} +23 -23
- package/dist/worker-c1e68hpa.js +78 -0
- package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
- package/openapi.json +491 -52
- package/package.json +3 -3
- package/src/apps/action-runner.ts +146 -0
- package/src/apps/catalog.generated.json +3 -2
- package/src/apps/definition.ts +152 -4
- package/src/apps/row-store.ts +23 -9
- package/src/apps/schema-migrate.ts +14 -6
- package/src/apps/script-types.ts +2 -1
- package/src/apps/sync.ts +20 -19
- package/src/be/db/tasks/read.ts +112 -0
- package/src/be/db.ts +16 -4
- package/src/be/fleet-benchmark-stats.ts +10 -5
- package/src/be/models-catalog.ts +4 -0
- package/src/be/onboarding.ts +6 -0
- package/src/be/scripts/typecheck.ts +13 -0
- package/src/be/swarm-config-guard.ts +25 -1
- package/src/be/telemetry-enable.ts +1 -1
- package/src/commands/provider-credentials.ts +12 -2
- package/src/commands/runner.ts +3 -0
- package/src/heartbeat/heartbeat.ts +2 -2
- package/src/hooks/codex-hook.ts +6 -2
- package/src/hooks/hook.ts +7 -0
- package/src/hooks/pr-body-guard.ts +38 -11
- package/src/hooks/pr-room-guard.ts +88 -0
- package/src/http/agents.ts +1 -0
- package/src/http/apps.ts +99 -183
- package/src/http/fleet-benchmark.ts +79 -30
- package/src/http/global-config.ts +7 -0
- package/src/http/ranking.ts +24 -40
- package/src/http/status.ts +49 -43
- package/src/http/tasks.ts +49 -1
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/used-by.ts +55 -19
- package/src/http/users.ts +2 -2
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +15 -0
- package/src/providers/reasoning-effort.ts +2 -1
- package/src/providers/types.ts +2 -0
- package/src/ranking/telemetry-ranking.ts +5 -33
- package/src/scripts-runtime/sdk-allowlist.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +31 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
- package/src/server.ts +4 -0
- package/src/telemetry-context.ts +19 -1
- package/src/telemetry-contract/EVENTS.md +42 -8
- package/src/telemetry-contract/SOURCE.json +6 -5
- package/src/telemetry-contract/catalog.json +225 -5
- package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
- package/src/telemetry-contract/types.gen.ts +225 -27
- package/src/telemetry-identity.ts +47 -9
- package/src/telemetry-snapshot.ts +9 -1
- package/src/telemetry-ui-events.ts +9 -2
- package/src/telemetry.ts +29 -6
- package/src/tests/app-action-run-tool.test.ts +178 -0
- package/src/tests/app-row-tools.test.ts +385 -0
- package/src/tests/apps-json-column.test.ts +611 -0
- package/src/tests/apps-script-types.test.ts +2 -0
- package/src/tests/apps-sync-engine.test.ts +42 -0
- package/src/tests/check-pr-body.test.ts +0 -23
- package/src/tests/credential-check.test.ts +38 -0
- package/src/tests/db-task-read-contract.test.ts +4 -0
- package/src/tests/db-task-write-contract.test.ts +2 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/fleet-benchmark.test.ts +122 -2
- package/src/tests/http-users.test.ts +62 -1
- package/src/tests/model-catalog-table.test.ts +63 -0
- package/src/tests/model-profiles-api.test.ts +19 -7
- package/src/tests/models-catalog.test.ts +50 -0
- package/src/tests/onboarding.test.ts +21 -0
- package/src/tests/org-website.test.ts +62 -0
- package/src/tests/pi-mono-adapter.test.ts +23 -0
- package/src/tests/pr-body-leaks.test.ts +4 -0
- package/src/tests/pr-room-guard.test.ts +242 -0
- package/src/tests/ranking-listing.test.ts +8 -23
- package/src/tests/ranking-route.test.ts +49 -47
- package/src/tests/reasoning-effort.test.ts +39 -3
- package/src/tests/runner-harness-telemetry-env.test.ts +62 -1
- package/src/tests/status.test.ts +158 -9
- package/src/tests/swarm-tool-result-gate.test.ts +2 -4
- package/src/tests/task-lineage.test.ts +180 -0
- package/src/tests/telemetry-context.test.ts +128 -2
- package/src/tests/telemetry-init.test.ts +62 -5
- package/src/tests/telemetry-ui-route.test.ts +123 -0
- package/src/tests/tool-annotations.test.ts +3 -2
- package/src/tests/used-by-route.test.ts +144 -10
- package/src/tests/user-token-rest-auth.test.ts +38 -0
- package/src/tests/workflow-agent-task-retry.test.ts +130 -4
- package/src/tests/workflow-wait-recovery.test.ts +116 -4
- package/src/tools/app-action-run.ts +63 -0
- package/src/tools/app-rows.ts +239 -0
- package/src/tools/model-catalog/index.ts +1 -1
- package/src/tools/tool-config.ts +8 -0
- package/src/tools/utils.ts +1 -1
- package/src/tools/workflows/retry-workflow-run.ts +1 -1
- package/src/types.ts +17 -0
- package/src/utils/company-profile.ts +24 -0
- package/src/utils/org-website.ts +58 -21
- package/src/workflows/recovery.ts +10 -22
- package/src/workflows/resume.ts +16 -0
- package/src/workflows/task-step-routing.ts +47 -7
- package/templates/skills/apps/SKILL.md +44 -4
- package/templates/skills/apps/content.md +44 -4
- package/dist/handlers-zbs90bny.js +0 -39
- package/dist/index-8pwc1jc8.js +0 -40
- package/dist/index-nxa6me0q.js +0 -41
- package/dist/lead-td8h2sfn.js +0 -78
- package/dist/worker-wr6xgfr2.js +0 -78
- package/src/tests/check-ranking-entry.test.ts +0 -146
package/src/http/ranking.ts
CHANGED
|
@@ -3,13 +3,9 @@ import { z } from "zod";
|
|
|
3
3
|
import { getDbClient, upsertSwarmConfig } from "../be/db";
|
|
4
4
|
import {
|
|
5
5
|
buildListing,
|
|
6
|
-
mintRankingClaim,
|
|
7
6
|
PUBLIC_RANKING_ENABLED_KEY,
|
|
8
7
|
RANKING_CATEGORIES,
|
|
9
|
-
rankingEntryFile,
|
|
10
8
|
rankingFormSchema,
|
|
11
|
-
rankingLeaveUrl,
|
|
12
|
-
rankingPrUrl,
|
|
13
9
|
readTelemetryRanking,
|
|
14
10
|
TELEMETRY_RANKING_KEY,
|
|
15
11
|
type TelemetryRanking,
|
|
@@ -23,14 +19,14 @@ import {
|
|
|
23
19
|
} from "../utils/org-identity";
|
|
24
20
|
import { ensureConfigAdmin } from "./config";
|
|
25
21
|
import { scheduleIntegrationsReload } from "./core";
|
|
22
|
+
import { globalConfig } from "./global-config";
|
|
26
23
|
import { route } from "./route-def";
|
|
27
|
-
import {
|
|
24
|
+
import { telemetryProxyEndpoint } from "./used-by";
|
|
28
25
|
|
|
29
26
|
// ─── Schemas ─────────────────────────────────────────────────────────────────
|
|
30
27
|
|
|
31
28
|
const ListingSchema = z.object({
|
|
32
29
|
v: z.literal(1),
|
|
33
|
-
claim: z.string(),
|
|
34
30
|
name: z.string(),
|
|
35
31
|
domain: z.string(),
|
|
36
32
|
category: z.enum(RANKING_CATEGORIES),
|
|
@@ -49,26 +45,21 @@ const RankingStateSchema = z.object({
|
|
|
49
45
|
name: z.string().nullable(),
|
|
50
46
|
domain: z.string().nullable(),
|
|
51
47
|
listing: ListingSchema.nullable(),
|
|
52
|
-
/** False when the stored listing was built for another website:
|
|
48
|
+
/** False when the stored listing was built for another website: Get ranked again. */
|
|
53
49
|
listingMatchesDomain: z.boolean(),
|
|
54
|
-
|
|
55
|
-
prUrl: z.string().nullable(),
|
|
50
|
+
/** The proxy page that checks whether the domain would be ranked. */
|
|
56
51
|
verifyUrl: z.string().nullable(),
|
|
57
52
|
});
|
|
58
53
|
export type RankingState = z.infer<typeof RankingStateSchema>;
|
|
59
54
|
|
|
60
55
|
const OptInResponseSchema = z.object({
|
|
61
56
|
domain: z.string(),
|
|
62
|
-
claim: z.string(),
|
|
63
57
|
listing: ListingSchema,
|
|
64
|
-
fileJson: z.string(),
|
|
65
|
-
prUrl: z.string(),
|
|
66
58
|
verifyUrl: z.string(),
|
|
67
59
|
});
|
|
68
60
|
|
|
69
61
|
const OptOutResponseSchema = z.object({
|
|
70
62
|
enabled: z.literal(false),
|
|
71
|
-
leaveUrl: z.string().nullable(),
|
|
72
63
|
});
|
|
73
64
|
|
|
74
65
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
@@ -79,7 +70,7 @@ const getRanking = route({
|
|
|
79
70
|
pattern: ["api", "ranking"],
|
|
80
71
|
summary: "Public ranking state",
|
|
81
72
|
description:
|
|
82
|
-
"Whether this install can join the public agent-swarm.dev ranking (telemetry on, organization name and website set), whether it is opted in, and its stored listing.
|
|
73
|
+
"Whether this install can join the public agent-swarm.dev ranking (telemetry on, organization name and website set), whether it is opted in, and its stored listing. Only a config admin can read it.",
|
|
83
74
|
tags: ["Telemetry"],
|
|
84
75
|
responses: {
|
|
85
76
|
200: { description: "Ranking state", schema: RankingStateSchema },
|
|
@@ -95,7 +86,7 @@ const optIn = route({
|
|
|
95
86
|
pattern: ["api", "ranking", "opt-in"],
|
|
96
87
|
summary: "Opt this install into the public ranking",
|
|
97
88
|
description:
|
|
98
|
-
"Builds the telemetry_ranking listing from the organization name and website plus the chosen category, stores it, turns PUBLIC_RANKING_ENABLED on and sends ranking.opted_in.
|
|
89
|
+
"Builds the telemetry_ranking listing from the organization name and website plus the chosen category, stores it, turns PUBLIC_RANKING_ENABLED on and sends ranking.opted_in. That is the whole opt-in: the install shows on agent-swarm.dev/ranking at the next hourly refresh.",
|
|
99
90
|
tags: ["Telemetry"],
|
|
100
91
|
body: rankingFormSchema,
|
|
101
92
|
responses: {
|
|
@@ -117,7 +108,7 @@ const optOut = route({
|
|
|
117
108
|
pattern: ["api", "ranking", "opt-out"],
|
|
118
109
|
summary: "Leave the public ranking",
|
|
119
110
|
description:
|
|
120
|
-
"Turns PUBLIC_RANKING_ENABLED off and sends ranking.opted_out, so workers stop sending ranking.usage.
|
|
111
|
+
"Turns PUBLIC_RANKING_ENABLED off and sends ranking.opted_out, so workers stop sending ranking.usage. The install leaves agent-swarm.dev/ranking at the next hourly refresh.",
|
|
121
112
|
tags: ["Telemetry"],
|
|
122
113
|
responses: {
|
|
123
114
|
200: { description: "Opted out", schema: OptOutResponseSchema },
|
|
@@ -163,11 +154,17 @@ async function telemetryReady(): Promise<boolean> {
|
|
|
163
154
|
return isTelemetryEnabled() && Boolean(await globalConfig("telemetry_installation_id"));
|
|
164
155
|
}
|
|
165
156
|
|
|
166
|
-
async function verifyUrl(
|
|
167
|
-
const params = new URLSearchParams({ domain
|
|
157
|
+
async function verifyUrl(domain: string): Promise<string> {
|
|
158
|
+
const params = new URLSearchParams({ domain });
|
|
168
159
|
return `${await telemetryProxyEndpoint("/v1/ranking/verify")}?${params.toString()}`;
|
|
169
160
|
}
|
|
170
161
|
|
|
162
|
+
/** The listing without the claim a v2.4.0 opt-in stored. */
|
|
163
|
+
function withoutClaim(listing: TelemetryRanking): Omit<TelemetryRanking, "claim"> {
|
|
164
|
+
const { claim: _claim, ...rest } = listing;
|
|
165
|
+
return rest;
|
|
166
|
+
}
|
|
167
|
+
|
|
171
168
|
async function setEnabled(enabled: boolean): Promise<void> {
|
|
172
169
|
await upsertSwarmConfig({
|
|
173
170
|
scope: "global",
|
|
@@ -179,7 +176,7 @@ async function setEnabled(enabled: boolean): Promise<void> {
|
|
|
179
176
|
function emitOptedIn(listing: TelemetryRanking): void {
|
|
180
177
|
telemetry.ranking(
|
|
181
178
|
"opted_in",
|
|
182
|
-
{ listing: JSON.stringify(listing) },
|
|
179
|
+
{ listing: JSON.stringify(withoutClaim(listing)) },
|
|
183
180
|
{ orgName: listing.name, orgWebsite: listing.domain },
|
|
184
181
|
);
|
|
185
182
|
}
|
|
@@ -204,17 +201,15 @@ export async function buildRankingState(): Promise<RankingState> {
|
|
|
204
201
|
enabled: enabled && listingMatchesDomain,
|
|
205
202
|
name: identity.name,
|
|
206
203
|
domain: identity.website,
|
|
207
|
-
listing,
|
|
204
|
+
listing: listing ? withoutClaim(listing) : null,
|
|
208
205
|
listingMatchesDomain,
|
|
209
|
-
|
|
210
|
-
prUrl: current ? rankingPrUrl(current) : null,
|
|
211
|
-
verifyUrl: current ? await verifyUrl(current) : null,
|
|
206
|
+
verifyUrl: current ? await verifyUrl(current.domain) : null,
|
|
212
207
|
};
|
|
213
208
|
}
|
|
214
209
|
|
|
215
210
|
/**
|
|
216
211
|
* Keep the listing in step with the org identity after a global config write.
|
|
217
|
-
* A renamed org rebuilds the listing
|
|
212
|
+
* A renamed org rebuilds the listing and re-sends it; a
|
|
218
213
|
* cleared or placeholder name, a changed or cleared website, or the setting
|
|
219
214
|
* turned off by hand opts the install out.
|
|
220
215
|
*/
|
|
@@ -238,7 +233,7 @@ export async function syncRankingAfterConfigChange(
|
|
|
238
233
|
if (identity.ok && identity.website === listing.domain) {
|
|
239
234
|
if (identity.name === listing.name) return;
|
|
240
235
|
try {
|
|
241
|
-
rebuilt = buildListing(identity, listing
|
|
236
|
+
rebuilt = buildListing(identity, listing);
|
|
242
237
|
} catch {
|
|
243
238
|
// A name the listing cannot carry (over 60 characters) leaves the ranking.
|
|
244
239
|
}
|
|
@@ -272,7 +267,6 @@ export async function handleRanking(
|
|
|
272
267
|
if (getRanking.match(req.method, pathSegments)) {
|
|
273
268
|
const parsed = await getRanking.parse(req, res, pathSegments, queryParams);
|
|
274
269
|
if (!parsed) return true;
|
|
275
|
-
// The state carries the claim and the PR that proves ownership of the listing.
|
|
276
270
|
if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
|
|
277
271
|
getRanking.respond(res, 200, await buildRankingState());
|
|
278
272
|
return true;
|
|
@@ -300,16 +294,12 @@ export async function handleRanking(
|
|
|
300
294
|
return true;
|
|
301
295
|
}
|
|
302
296
|
|
|
303
|
-
const website = identity.website;
|
|
304
297
|
const db = getDbClient();
|
|
305
|
-
//
|
|
306
|
-
// must return the same claim, or the one merged into community/ranking is lost.
|
|
298
|
+
// The listing and the flag land together, and the event fires only after both commit.
|
|
307
299
|
const built = await db.transaction(async () => {
|
|
308
|
-
const stored = await storedListing();
|
|
309
|
-
const claim = stored?.domain === website ? stored.claim : mintRankingClaim();
|
|
310
300
|
let listing: TelemetryRanking;
|
|
311
301
|
try {
|
|
312
|
-
listing = buildListing(identity, parsed.body
|
|
302
|
+
listing = buildListing(identity, parsed.body);
|
|
313
303
|
} catch (error) {
|
|
314
304
|
return { ok: false as const, error };
|
|
315
305
|
}
|
|
@@ -339,11 +329,8 @@ export async function handleRanking(
|
|
|
339
329
|
|
|
340
330
|
optIn.respond(res, 200, {
|
|
341
331
|
domain: listing.domain,
|
|
342
|
-
claim: listing.claim,
|
|
343
332
|
listing,
|
|
344
|
-
|
|
345
|
-
prUrl: rankingPrUrl(listing),
|
|
346
|
-
verifyUrl: await verifyUrl(listing),
|
|
333
|
+
verifyUrl: await verifyUrl(listing.domain),
|
|
347
334
|
});
|
|
348
335
|
return true;
|
|
349
336
|
}
|
|
@@ -356,10 +343,7 @@ export async function handleRanking(
|
|
|
356
343
|
await setEnabled(false);
|
|
357
344
|
if (listing) emitOptedOut(listing.domain);
|
|
358
345
|
scheduleIntegrationsReload();
|
|
359
|
-
optOut.respond(res, 200, {
|
|
360
|
-
enabled: false,
|
|
361
|
-
leaveUrl: listing ? rankingLeaveUrl(listing.domain) : null,
|
|
362
|
-
});
|
|
346
|
+
optOut.respond(res, 200, { enabled: false });
|
|
363
347
|
return true;
|
|
364
348
|
}
|
|
365
349
|
|
package/src/http/status.ts
CHANGED
|
@@ -197,7 +197,7 @@ export type AutomationStatus = z.infer<typeof AutomationStatusSchema>;
|
|
|
197
197
|
* - `broken` — harness or workers blocking (creds missing, no live workers ever).
|
|
198
198
|
* - `degraded` — at least one optional integration `unverified`/`configured`
|
|
199
199
|
* while another is configured, OR harness creds present but
|
|
200
|
-
* never
|
|
200
|
+
* the latest live test failed or never ran (`configured`).
|
|
201
201
|
* - `ok` — harness + workers `verified`; no integration left in
|
|
202
202
|
* `configured` state.
|
|
203
203
|
*/
|
|
@@ -237,12 +237,12 @@ export const TestConnectionResponseSchema = z.object({
|
|
|
237
237
|
/**
|
|
238
238
|
* Read all worker reports for a given harness provider and reduce them to a
|
|
239
239
|
* single milestone-grade rollup. Reports are joined from `agents.cred_status`
|
|
240
|
-
* (migration 055) — no provider-adapter code runs here.
|
|
240
|
+
* (migration 055) — no provider-adapter code runs here. Offline agents are
|
|
241
|
+
* excluded: `workers` and `reports` count online agents only.
|
|
241
242
|
*
|
|
242
|
-
* `verified` ⇐
|
|
243
|
-
* is fresher than `getCredVerifyTtlMs()`.
|
|
243
|
+
* `verified` ⇐ the provider's most recent live test passed, whatever its age.
|
|
244
244
|
* `configured` ⇐ at least one worker has `ready: true` (presence check
|
|
245
|
-
* passed) but
|
|
245
|
+
* passed) but the latest live test failed or none ever ran.
|
|
246
246
|
* `unverified` ⇐ no worker reported, or all reporting workers have
|
|
247
247
|
* `ready: false`.
|
|
248
248
|
*/
|
|
@@ -258,14 +258,6 @@ export interface CredRollup {
|
|
|
258
258
|
oldestReportAgeMs: number | null;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
function getCredVerifyTtlMs(): number {
|
|
262
|
-
const raw = process.env.SWARM_VERIFY_TTL_MS;
|
|
263
|
-
if (!raw) return 3_600_000; // 1h default — matches pre-refactor behavior.
|
|
264
|
-
const parsed = Number.parseInt(raw, 10);
|
|
265
|
-
if (Number.isNaN(parsed) || parsed <= 0) return 3_600_000;
|
|
266
|
-
return parsed;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
261
|
/**
|
|
270
262
|
* Compatibility stub. The pre-refactor in-memory test-connection cache is
|
|
271
263
|
* gone — credential state now lives on agent rows. Tests still call this
|
|
@@ -276,7 +268,11 @@ export function _resetTestConnectionCache(): void {
|
|
|
276
268
|
}
|
|
277
269
|
|
|
278
270
|
export async function rollupCredStatusForProvider(provider: string): Promise<CredRollup> {
|
|
279
|
-
|
|
271
|
+
// An offline agent keeps its last cred_status after `/close`, so only online
|
|
272
|
+
// agents decide the rollup; a stale failure must not override a live pass.
|
|
273
|
+
const agents = (await listAgentsWithCredStatusByProvider(provider)).filter(
|
|
274
|
+
(a) => a.status !== "offline",
|
|
275
|
+
);
|
|
280
276
|
const reports = agents.map((a) => a.credStatus).filter((s): s is AgentCredStatus => s != null);
|
|
281
277
|
|
|
282
278
|
if (reports.length === 0) {
|
|
@@ -291,23 +287,19 @@ export async function rollupCredStatusForProvider(provider: string): Promise<Cre
|
|
|
291
287
|
};
|
|
292
288
|
}
|
|
293
289
|
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
290
|
+
// The provider's latest live test decides, whatever its age. Workers run the
|
|
291
|
+
// test at boot and re-run it only when credentials change, so an old pass is
|
|
292
|
+
// the normal state of a long-running worker, not a missing check.
|
|
297
293
|
const now = Date.now();
|
|
298
|
-
const
|
|
299
|
-
report.liveTest?.ok === true && now - (report.liveTest?.testedAt ?? 0) < ttl;
|
|
300
|
-
const passing = reports
|
|
301
|
-
.filter(isFreshPassingReport)
|
|
302
|
-
.sort((a, b) => (b.liveTest?.testedAt ?? 0) - (a.liveTest?.testedAt ?? 0));
|
|
294
|
+
const passing = reports.filter((r) => r.liveTest?.ok === true);
|
|
303
295
|
const anyLive = reports
|
|
304
296
|
.filter((r) => r.liveTest != null)
|
|
305
297
|
.sort((a, b) => (b.liveTest?.testedAt ?? 0) - (a.liveTest?.testedAt ?? 0));
|
|
306
|
-
const latestLiveTest =
|
|
298
|
+
const latestLiveTest = anyLive[0]?.liveTest ?? null;
|
|
307
299
|
|
|
308
300
|
const anyReady = reports.some((r) => r.ready);
|
|
309
301
|
const state: CredRollupState =
|
|
310
|
-
|
|
302
|
+
latestLiveTest?.ok === true ? "verified" : anyReady ? "configured" : "unverified";
|
|
311
303
|
|
|
312
304
|
// For UI hints, pick the most-recent missing[] from a not-ready report.
|
|
313
305
|
const latestNotReady = reports
|
|
@@ -349,23 +341,19 @@ function buildIdentity(): StatusIdentity {
|
|
|
349
341
|
// ─── Setup milestones ────────────────────────────────────────────────────────
|
|
350
342
|
|
|
351
343
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
* "1 worker · live test ok"
|
|
355
|
-
* "2 workers · presence ok, awaiting live test"
|
|
356
|
-
* "missing: OPENAI_API_KEY"
|
|
344
|
+
* One short phrase for a provider that needs attention, e.g. "live test
|
|
345
|
+
* failed" or "missing: OPENAI_API_KEY". A verified provider gets none.
|
|
357
346
|
*/
|
|
358
|
-
function describeRoll(roll: CredRollup): string {
|
|
347
|
+
function describeRoll(roll: CredRollup): string | null {
|
|
348
|
+
if (roll.state === "verified") return null;
|
|
359
349
|
if (roll.workers === 0) return "no workers";
|
|
360
|
-
if (roll.reports === 0)
|
|
361
|
-
|
|
350
|
+
if (roll.reports === 0) return "no credential report";
|
|
351
|
+
if (roll.state === "configured") {
|
|
352
|
+
return roll.latestLiveTest?.ok === false ? "live test failed" : "not live-tested";
|
|
362
353
|
}
|
|
363
|
-
const w = `${roll.workers} ${roll.workers === 1 ? "worker" : "workers"}`;
|
|
364
|
-
if (roll.state === "verified") return `${w} · live test ok`;
|
|
365
|
-
if (roll.state === "configured") return `${w} · presence ok, awaiting live test`;
|
|
366
354
|
return roll.latestMissing.length > 0
|
|
367
355
|
? `missing: ${roll.latestMissing.join(", ")}`
|
|
368
|
-
: "
|
|
356
|
+
: "credentials blocked";
|
|
369
357
|
}
|
|
370
358
|
|
|
371
359
|
/**
|
|
@@ -375,7 +363,7 @@ function describeRoll(roll: CredRollup): string {
|
|
|
375
363
|
* workers via migration 054/055), runs the cred-status rollup per provider,
|
|
376
364
|
* and aggregates:
|
|
377
365
|
*
|
|
378
|
-
* `verified` — every provider
|
|
366
|
+
* `verified` — every provider's latest live test passed.
|
|
379
367
|
* `configured` — every provider has at least `configured`, ≥1 not verified.
|
|
380
368
|
* `unverified` — any provider has `unverified` (no reports OR all-blocked).
|
|
381
369
|
*
|
|
@@ -398,13 +386,25 @@ async function harnessMilestone(): Promise<SetupMilestone> {
|
|
|
398
386
|
}
|
|
399
387
|
|
|
400
388
|
const perProvider: { provider: z.infer<typeof ProviderNameSchema>; roll: CredRollup }[] = [];
|
|
389
|
+
let recognised = 0;
|
|
401
390
|
for (const { provider } of fleet) {
|
|
402
391
|
const parsed = ProviderNameSchema.safeParse(provider);
|
|
403
392
|
if (!parsed.success) continue;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
393
|
+
recognised++;
|
|
394
|
+
const roll = await rollupCredStatusForProvider(parsed.data);
|
|
395
|
+
// A provider whose agents are all offline has nothing live to judge.
|
|
396
|
+
if (roll.workers === 0) continue;
|
|
397
|
+
perProvider.push({ provider: parsed.data, roll });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (recognised > 0 && perProvider.length === 0) {
|
|
401
|
+
return {
|
|
402
|
+
id: "harness",
|
|
403
|
+
label: "Harness configured",
|
|
404
|
+
state: "unverified",
|
|
405
|
+
hint: "No agents online. Start a worker to run a live credential test.",
|
|
406
|
+
action_url: "/agents",
|
|
407
|
+
};
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
if (perProvider.length === 0) {
|
|
@@ -424,7 +424,13 @@ async function harnessMilestone(): Promise<SetupMilestone> {
|
|
|
424
424
|
? "configured"
|
|
425
425
|
: "unverified";
|
|
426
426
|
|
|
427
|
-
const hint =
|
|
427
|
+
const hint =
|
|
428
|
+
perProvider
|
|
429
|
+
.flatMap((p) => {
|
|
430
|
+
const phrase = describeRoll(p.roll);
|
|
431
|
+
return phrase ? [`${p.provider}: ${phrase}`] : [];
|
|
432
|
+
})
|
|
433
|
+
.join(" · ") || undefined;
|
|
428
434
|
const singleProvider = perProvider.length === 1 ? perProvider[0]?.provider : undefined;
|
|
429
435
|
|
|
430
436
|
return {
|
|
@@ -848,7 +854,7 @@ export async function handleStatus(
|
|
|
848
854
|
if (roll.workers === 0) {
|
|
849
855
|
json(res, {
|
|
850
856
|
ok: false,
|
|
851
|
-
error: `No workers
|
|
857
|
+
error: `No online workers with HARNESS_PROVIDER=${provider}. Start a worker — its boot loop will run a live test and report it here.`,
|
|
852
858
|
latency_ms: 0,
|
|
853
859
|
});
|
|
854
860
|
return true;
|
package/src/http/tasks.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
getSteeringMessagesForTask,
|
|
21
21
|
getTaskAttachments,
|
|
22
22
|
getTaskById,
|
|
23
|
+
getTaskLineage,
|
|
23
24
|
getTasksCount,
|
|
24
25
|
markSteeringDelivered,
|
|
25
26
|
markSteeringHandled,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
resumeTask,
|
|
30
31
|
settleSupersededTaskDependents,
|
|
31
32
|
supersedeTask,
|
|
33
|
+
TASK_LINEAGE_LIMITS,
|
|
32
34
|
updateAgentStatusFromCapacity,
|
|
33
35
|
updateTaskClaudeSessionId,
|
|
34
36
|
updateTaskProgress,
|
|
@@ -73,6 +75,7 @@ import {
|
|
|
73
75
|
SteeringSourceSchema,
|
|
74
76
|
SteerModeSchema,
|
|
75
77
|
SteerResultSchema,
|
|
78
|
+
SUPERSEDED_WORKFLOW_TASK_REASON,
|
|
76
79
|
splitLegacyModelAlias,
|
|
77
80
|
TaskAttachmentSchema,
|
|
78
81
|
} from "../types";
|
|
@@ -577,6 +580,33 @@ const getTask = route({
|
|
|
577
580
|
},
|
|
578
581
|
});
|
|
579
582
|
|
|
583
|
+
const getTaskLineageRoute = route({
|
|
584
|
+
method: "get",
|
|
585
|
+
path: "/api/tasks/{id}/lineage",
|
|
586
|
+
pattern: ["api", "tasks", null, "lineage"],
|
|
587
|
+
session: { taskParam: "id" },
|
|
588
|
+
summary: "Get the tree a task belongs to (ancestors and descendants)",
|
|
589
|
+
description: `Follows \`parentTaskId\` up to the topmost ancestor, then returns every descendant of that root as slim task rows, the task's own subtree first. Bounded: at most ${TASK_LINEAGE_LIMITS.maxAncestors} hops up, \`maxDepth\` levels down and about twice \`maxNodes\` rows; \`truncated\` is true when any task in the tree is missing from the response.`,
|
|
590
|
+
tags: ["Tasks"],
|
|
591
|
+
params: z.object({ id: z.string() }),
|
|
592
|
+
query: z.object({
|
|
593
|
+
maxNodes: z.coerce.number().int().min(1).max(TASK_LINEAGE_LIMITS.maxNodes).optional(),
|
|
594
|
+
maxDepth: z.coerce.number().int().min(1).max(TASK_LINEAGE_LIMITS.maxDepth).optional(),
|
|
595
|
+
}),
|
|
596
|
+
responses: {
|
|
597
|
+
200: {
|
|
598
|
+
description: "The lineage tree as a flat list linked by `parentTaskId`",
|
|
599
|
+
schema: z.object({
|
|
600
|
+
rootTaskId: z.string(),
|
|
601
|
+
tasks: z.array(AgentTaskSummarySchema.extend({ workflowRunId: z.string().optional() })),
|
|
602
|
+
truncated: z.boolean(),
|
|
603
|
+
}),
|
|
604
|
+
},
|
|
605
|
+
403: { description: "A user caller cannot read the task's session" },
|
|
606
|
+
404: { description: "Task not found" },
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
|
|
580
610
|
const updateTaskProgressRoute = route({
|
|
581
611
|
method: "post",
|
|
582
612
|
path: "/api/tasks/{id}/progress",
|
|
@@ -1120,6 +1150,8 @@ export async function handleTasks(
|
|
|
1120
1150
|
offeredTo: parsed.body.offeredTo || undefined,
|
|
1121
1151
|
dir: parsed.body.dir || undefined,
|
|
1122
1152
|
parentTaskId: parsed.body.parentTaskId || undefined,
|
|
1153
|
+
// Human chat starts a new output contract; agent children keep inheriting.
|
|
1154
|
+
inheritParentOutputSchema: principal.kind === "agent",
|
|
1123
1155
|
source: parsed.body.source || "api",
|
|
1124
1156
|
outputSchema: parsed.body.outputSchema || undefined,
|
|
1125
1157
|
contextKey: parsed.body.contextKey || undefined,
|
|
@@ -1614,6 +1646,22 @@ export async function handleTasks(
|
|
|
1614
1646
|
return true;
|
|
1615
1647
|
}
|
|
1616
1648
|
|
|
1649
|
+
if (getTaskLineageRoute.match(req.method, pathSegments)) {
|
|
1650
|
+
const parsed = await getTaskLineageRoute.parse(req, res, pathSegments, queryParams);
|
|
1651
|
+
if (!parsed) return true;
|
|
1652
|
+
// Every task in the tree shares the session root, so the path posture covers them all.
|
|
1653
|
+
const lineage = await getTaskLineage(parsed.params.id, {
|
|
1654
|
+
maxNodes: parsed.query.maxNodes,
|
|
1655
|
+
maxDepth: parsed.query.maxDepth,
|
|
1656
|
+
});
|
|
1657
|
+
if (!lineage) {
|
|
1658
|
+
jsonError(res, "Task not found", 404);
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
getTaskLineageRoute.respond(res, 200, lineage);
|
|
1662
|
+
return true;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1617
1665
|
if (updateTaskProgressRoute.match(req.method, pathSegments)) {
|
|
1618
1666
|
const parsed = await updateTaskProgressRoute.parse(req, res, pathSegments, queryParams);
|
|
1619
1667
|
if (!parsed) return true;
|
|
@@ -2014,7 +2062,7 @@ export async function handleTasks(
|
|
|
2014
2062
|
// Check this BEFORE the supersede UPDATE so we don't leave a workflow
|
|
2015
2063
|
// step in `superseded` if the engine expects `failed`.
|
|
2016
2064
|
if (task.workflowRunStepId != null) {
|
|
2017
|
-
const failed = await failTask(parsed.params.id,
|
|
2065
|
+
const failed = await failTask(parsed.params.id, SUPERSEDED_WORKFLOW_TASK_REASON);
|
|
2018
2066
|
ensure({
|
|
2019
2067
|
id: "task.workflow_step_failed_on_supersede",
|
|
2020
2068
|
flow: "task",
|
package/src/http/telemetry-ui.ts
CHANGED
|
@@ -43,7 +43,7 @@ const enableTelemetry = route({
|
|
|
43
43
|
description:
|
|
44
44
|
"Stores `ANONYMIZED_TELEMETRY=true` and applies it to the running server: the install ID is minted if needed and `telemetry.enabled` is sent once, so no restart is needed.",
|
|
45
45
|
tags: ["Telemetry"],
|
|
46
|
-
body: z.object({ via: z.enum(["banner", "settings"]).default("banner") }),
|
|
46
|
+
body: z.object({ via: z.enum(["banner", "settings", "benchmark"]).default("banner") }),
|
|
47
47
|
responses: {
|
|
48
48
|
200: {
|
|
49
49
|
description: "Telemetry is on",
|
package/src/http/used-by.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { getDbClient, upsertSwarmConfig } from "../be/db";
|
|
4
4
|
import { isTelemetryEnabled, trackRelayed } from "../telemetry";
|
|
5
|
+
import { USER_FUNCTIONS } from "../utils/company-profile";
|
|
5
6
|
import { orgIdentityStatus } from "../utils/org-identity";
|
|
6
7
|
import { normalizeOrgWebsite, TELEMETRY_PROXY_ORIGIN } from "../utils/org-website";
|
|
7
8
|
import { ensureConfigAdmin } from "./config";
|
|
8
9
|
import { scheduleIntegrationsReload } from "./core";
|
|
9
10
|
import { resolveHttpFavoriteOwner } from "./favorite-owner";
|
|
11
|
+
import { globalConfig } from "./global-config";
|
|
10
12
|
import { route } from "./route-def";
|
|
11
13
|
import { jsonError } from "./utils";
|
|
12
14
|
|
|
@@ -16,6 +18,9 @@ const CONSENT_AT_KEY = "SWARM_USED_BY_CONSENT_AT";
|
|
|
16
18
|
// still reaches the proxy after the name or website is changed or cleared.
|
|
17
19
|
const GRANTED_NAME_KEY = "SWARM_USED_BY_ORG_NAME";
|
|
18
20
|
const GRANTED_WEBSITE_KEY = "SWARM_USED_BY_ORG_WEBSITE";
|
|
21
|
+
const CONTACT_EMAIL_KEY = "SWARM_USED_BY_CONTACT_EMAIL";
|
|
22
|
+
const CONTACT_FUNCTION_KEY = "SWARM_USED_BY_CONTACT_FUNCTION";
|
|
23
|
+
const QUOTE_KEY = "SWARM_USED_BY_QUOTE";
|
|
19
24
|
const TIMEOUT_MS = 10_000;
|
|
20
25
|
|
|
21
26
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
@@ -26,17 +31,30 @@ const usedBy = route({
|
|
|
26
31
|
pattern: ["api", "used-by"],
|
|
27
32
|
summary: "Grant or revoke the 'used by' logo consent",
|
|
28
33
|
description:
|
|
29
|
-
"Records
|
|
34
|
+
"Records consent to show the organization's logo on agent-swarm.dev. A grant requires a valid contact email. It sends organization identity, contact details, and an optional quote to the proxy for manual review. This explicit admin action works when telemetry is off. A revoke clears the stored contact details and quote.",
|
|
30
35
|
tags: ["Telemetry"],
|
|
31
|
-
body: z
|
|
36
|
+
body: z
|
|
37
|
+
.object({
|
|
38
|
+
granted: z.boolean(),
|
|
39
|
+
contactEmail: z
|
|
40
|
+
.string()
|
|
41
|
+
.max(320, "Contact email must be at most 320 characters")
|
|
42
|
+
.email("Contact email must be a valid email address")
|
|
43
|
+
.optional(),
|
|
44
|
+
contactFunction: z.enum(USER_FUNCTIONS).optional(),
|
|
45
|
+
quote: z.string().max(280).optional(),
|
|
46
|
+
})
|
|
47
|
+
.refine((body) => !body.granted || body.contactEmail !== undefined, {
|
|
48
|
+
path: ["contactEmail"],
|
|
49
|
+
message: "Contact email is required when granting logo consent",
|
|
50
|
+
}),
|
|
32
51
|
responses: {
|
|
33
52
|
200: {
|
|
34
53
|
description: "Consent recorded",
|
|
35
54
|
schema: z.object({ granted: z.boolean(), changedAt: z.string() }),
|
|
36
55
|
},
|
|
37
56
|
400: {
|
|
38
|
-
description:
|
|
39
|
-
"Organization name or website is not set (a revoke falls back to the identity of the last grant)",
|
|
57
|
+
description: "Invalid contact details, or missing organization name or website",
|
|
40
58
|
},
|
|
41
59
|
401: { description: "Unauthorized" },
|
|
42
60
|
403: { description: "Only a config admin can change the consent" },
|
|
@@ -47,12 +65,6 @@ const usedBy = route({
|
|
|
47
65
|
|
|
48
66
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
49
67
|
|
|
50
|
-
/** A global config value, falling back to the process env. */
|
|
51
|
-
export async function globalConfig(key: string): Promise<string | undefined> {
|
|
52
|
-
const stored = (await getSwarmConfigs({ scope: "global", key }))?.[0]?.value?.trim();
|
|
53
|
-
return stored || process.env[key]?.trim() || undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
68
|
/** A proxy path next to the configured `/v1/feedback` endpoint, so a stubbed proxy gets every route. */
|
|
57
69
|
export async function telemetryProxyEndpoint(path: `/v1/${string}`): Promise<string> {
|
|
58
70
|
const feedback = await globalConfig("feedback_endpoint");
|
|
@@ -80,7 +92,7 @@ export async function handleUsedBy(
|
|
|
80
92
|
if (!parsed) return true;
|
|
81
93
|
if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
|
|
82
94
|
|
|
83
|
-
const { granted } = parsed.body;
|
|
95
|
+
const { granted, contactEmail, contactFunction, quote } = parsed.body;
|
|
84
96
|
// The shared identity reader. The grant keeps its own rule (any non-empty
|
|
85
97
|
// name), so it does not apply the ranking's placeholder-name check.
|
|
86
98
|
const current = orgIdentityStatus({
|
|
@@ -116,6 +128,9 @@ export async function handleUsedBy(
|
|
|
116
128
|
org_website: orgWebsite,
|
|
117
129
|
...(orgDomain ? { org_domain: orgDomain } : {}),
|
|
118
130
|
granted,
|
|
131
|
+
...(granted
|
|
132
|
+
? { contact_email: contactEmail, contact_function: contactFunction, quote }
|
|
133
|
+
: {}),
|
|
119
134
|
changed_at: changedAt,
|
|
120
135
|
}),
|
|
121
136
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
@@ -129,18 +144,39 @@ export async function handleUsedBy(
|
|
|
129
144
|
return true;
|
|
130
145
|
}
|
|
131
146
|
|
|
132
|
-
await
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
await getDbClient().transaction(async () => {
|
|
148
|
+
await upsertSwarmConfig({ scope: "global", key: CONSENT_KEY, value: String(granted) });
|
|
149
|
+
await upsertSwarmConfig({ scope: "global", key: CONSENT_AT_KEY, value: changedAt });
|
|
150
|
+
if (granted) {
|
|
151
|
+
await upsertSwarmConfig({ scope: "global", key: GRANTED_NAME_KEY, value: orgName });
|
|
152
|
+
await upsertSwarmConfig({ scope: "global", key: GRANTED_WEBSITE_KEY, value: orgWebsite });
|
|
153
|
+
}
|
|
154
|
+
// Contact details stay out of agent env. The email is also a secret row,
|
|
155
|
+
// so `GET /api/config` masks it for every reader.
|
|
156
|
+
for (const [key, value, isSecret] of [
|
|
157
|
+
[CONTACT_EMAIL_KEY, contactEmail, true],
|
|
158
|
+
[CONTACT_FUNCTION_KEY, contactFunction, false],
|
|
159
|
+
[QUOTE_KEY, quote, false],
|
|
160
|
+
] as const) {
|
|
161
|
+
await upsertSwarmConfig({
|
|
162
|
+
scope: "global",
|
|
163
|
+
key,
|
|
164
|
+
value: granted ? (value ?? "") : "",
|
|
165
|
+
isSecret,
|
|
166
|
+
exportToEnv: false,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
138
170
|
scheduleIntegrationsReload();
|
|
139
171
|
|
|
140
172
|
const owner = await resolveHttpFavoriteOwner(req, myAgentId);
|
|
141
173
|
trackRelayed(
|
|
142
174
|
"used_by.consent_changed",
|
|
143
|
-
{
|
|
175
|
+
{
|
|
176
|
+
granted,
|
|
177
|
+
has_quote: granted && Boolean(quote),
|
|
178
|
+
has_function: granted && Boolean(contactFunction),
|
|
179
|
+
},
|
|
144
180
|
{ actor: owner?.userId ? { userId: owner.userId } : undefined, emitter: "ui" },
|
|
145
181
|
);
|
|
146
182
|
usedBy.respond(res, 200, { granted, changedAt });
|
package/src/http/users.ts
CHANGED
|
@@ -205,7 +205,7 @@ const createUserRoute = route({
|
|
|
205
205
|
tags: ["Users"],
|
|
206
206
|
body: z.object({
|
|
207
207
|
name: z.string().min(1),
|
|
208
|
-
email: z.string().optional(),
|
|
208
|
+
email: z.string().email("Email must be a valid email address").optional(),
|
|
209
209
|
role: z.string().optional(),
|
|
210
210
|
notes: z.string().optional(),
|
|
211
211
|
emailAliases: z.array(z.string()).optional(),
|
|
@@ -307,7 +307,7 @@ const updateUserRoute = route({
|
|
|
307
307
|
body: z
|
|
308
308
|
.object({
|
|
309
309
|
name: z.string().min(1).optional(),
|
|
310
|
-
email: z.string().optional(),
|
|
310
|
+
email: z.string().email("Email must be a valid email address").nullable().optional(),
|
|
311
311
|
role: z.string().optional(),
|
|
312
312
|
notes: z.string().optional(),
|
|
313
313
|
emailAliases: z.array(z.string()).optional(),
|
|
@@ -359,7 +359,7 @@ const copilotTargetProfile: AcpTargetProfile = {
|
|
|
359
359
|
},
|
|
360
360
|
};
|
|
361
361
|
|
|
362
|
-
/** Qwen Code's `--auth-type` choices (0.25.
|
|
362
|
+
/** Qwen Code's `--auth-type` choices (0.25.3). `qwen-oauth` needs a browser, so it is left out. */
|
|
363
363
|
const QWEN_AUTH_TYPES = ["openai", "openai-responses", "anthropic", "gemini", "vertex-ai"] as const;
|
|
364
364
|
|
|
365
365
|
// Qwen Code is a Gemini CLI fork: QWEN_SYSTEM_MD replaces its built-in prompt
|
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
* default; the image installs exactly this version and the per-task settings
|
|
45
45
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
46
46
|
*/
|
|
47
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
47
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1792411243-gb8cffc";
|
|
48
48
|
|
|
49
49
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
50
50
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|