From c6c9fdf10c5da3aefc55cb97a71babcb8cdbf331 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Sun, 16 Aug 2026 16:35:33 -0600 Subject: [PATCH 1/4] fix(deps): move onto the live sandbox cohort and route a cataloged profile as a model The published tcloud 0.4.14 and tcloud-agent 0.3.3 depended on @tangle-network/sandbox ^0.9.5, which carries agent-interface 0.13.0 and agent-core 0.17.1. Installing either package beside a current first-party package therefore resolved two extra physical copies of agent-interface. sandbox 0.27.1 types BackendConfig.profile as an AgentProfile object only, so the SDK transport now sends a cataloged profile id as the model selector. That is the routing this module already documents for a string profile, and the one the bridge transport already applies. check:cohort-ranges runs before publish and fails when a first-party specifier names one version instead of a range. --- packages/tcloud-agent/package.json | 6 +- packages/tcloud-agent/src/agent-runner.ts | 21 +- .../tcloud-agent/tests/agent-runner.test.ts | 27 ++- packages/tcloud/package.json | 6 +- pnpm-lock.yaml | 213 +++++++++--------- pnpm-workspace.yaml | 4 + scripts/check-cohort-ranges.mjs | 58 +++++ 7 files changed, 217 insertions(+), 118 deletions(-) create mode 100644 scripts/check-cohort-ranges.mjs diff --git a/packages/tcloud-agent/package.json b/packages/tcloud-agent/package.json index e6afde6..0dd2186 100644 --- a/packages/tcloud-agent/package.json +++ b/packages/tcloud-agent/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/tcloud-agent", - "version": "0.3.3", + "version": "0.4.0", "description": "Agent run-loop primitive over Tangle sandbox transports — runs an AgentProfile against a brief with criterion gates, budget caps, and streaming events. Includes TangleToolProvider for Pi tool integration.", "type": "module", "main": "./dist/index.js", @@ -36,11 +36,11 @@ "build": "tsup src/index.ts src/pi-extension.ts --format esm --dts --clean", "dev": "tsx src/index.ts", "test": "vitest run", - "prepublishOnly": "pnpm build" + "prepublishOnly": "node ../../scripts/check-cohort-ranges.mjs && pnpm build" }, "dependencies": { "@sinclair/typebox": "^0.34.49", - "@tangle-network/sandbox": "^0.9.5", + "@tangle-network/sandbox": ">=0.27.1 <0.28.0", "@tangle-network/tcloud": "workspace:^", "viem": "^2.48.4" }, diff --git a/packages/tcloud-agent/src/agent-runner.ts b/packages/tcloud-agent/src/agent-runner.ts index efe660a..0e0eafa 100644 --- a/packages/tcloud-agent/src/agent-runner.ts +++ b/packages/tcloud-agent/src/agent-runner.ts @@ -54,11 +54,10 @@ import { TCloudClient, type ChatCompletion, type ChatCompletionChunk, type ChatM // ── Part types (wrappers over the sandbox SDK session-gateway shape) ───────── // // The sandbox SDK defines these in its `session-gateway/agent-connection.ts` -// (and canonically in `@tangle-network/agent-interface`). That package isn't -// published to the registry yet and older `@tangle-network/sandbox` builds do not -// re-export them, so we redeclare the minimal shape locally and re-export it -// for consumers. When the interface package ships we can flip these to a -// direct re-export without churning the consumer surface. +// and canonically in `@tangle-network/agent-interface`. `@tangle-network/sandbox` +// does not re-export them, so we redeclare the minimal shape here and re-export +// it for consumers. A direct `@tangle-network/agent-interface` dependency would +// let these become a re-export without churning the consumer surface. /** Text delta emitted by the sandbox sidecar as the model streams tokens. */ export interface TextPart { @@ -309,13 +308,17 @@ class SandboxSdkAgentSessionTransport implements AgentSessionTransport { start(input: AgentSessionStart): AgentSession { const sandbox = this.options.sandbox const sessionId = input.resume ?? this.options.sessionId + const backend = this.options.backend ?? {} + // `backend.profile` carries an inline profile definition only. A cataloged + // profile is an id, so it travels as the model selector — the same routing + // the bridge transport applies to a string profile. const promptOptions: PromptOptions = { sessionId, timeoutMs: this.options.timeoutMs, - backend: { - ...(this.options.backend ?? {}), - profile: input.profile, - }, + backend: + typeof input.profile === 'string' + ? { ...backend, model: { ...(backend.model ?? {}), model: input.profile } } + : { ...backend, profile: input.profile }, context: input.workspace?.dir ? { workspaceDir: input.workspace.dir } : undefined, } diff --git a/packages/tcloud-agent/tests/agent-runner.test.ts b/packages/tcloud-agent/tests/agent-runner.test.ts index 966c7bd..423576a 100644 --- a/packages/tcloud-agent/tests/agent-runner.test.ts +++ b/packages/tcloud-agent/tests/agent-runner.test.ts @@ -396,8 +396,33 @@ describe('Agent.run', () => { expect(prompts[0].message).toBe('hi') expect(prompts[0].options).toMatchObject({ sessionId: 'sdk-session', - backend: { profile: 'sf-proposer' }, + backend: { model: { model: 'sf-proposer' } }, }) + expect((prompts[0].options as { backend: { profile?: unknown } }).backend.profile).toBeUndefined() + }) + + it('Sandbox SDK transport sends an inline profile on backend.profile', async () => { + const prompts: Array<{ message: string; options: unknown }> = [] + const sandbox = { + async prompt(message: string, options: unknown) { + prompts.push({ message, options }) + return { success: true, response: 'sdk ok', durationMs: 12 } + }, + async *streamPrompt() { + throw new Error('streamPrompt should not be used when stream:false') + }, + } + const profile = { model: { default: 'kimi-k2' } } + await agent({ + transport: sandboxSdkTransport({ sandbox: sandbox as any }), + profile: profile as any, + brief: 'hi', + stream: false, + }).run() + expect(prompts[0].options).toMatchObject({ backend: { profile } }) + expect( + (prompts[0].options as { backend: { model?: { model?: string } } }).backend.model?.model, + ).toBeUndefined() }) it('forces non-streaming when usd budget is set so cost accounting can run', async () => { diff --git a/packages/tcloud/package.json b/packages/tcloud/package.json index 328a441..7ebfa83 100644 --- a/packages/tcloud/package.json +++ b/packages/tcloud/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/tcloud", - "version": "0.4.14", + "version": "0.5.0", "description": "TypeScript SDK and CLI for Tangle Router, Sandbox, model routing, and agent service calls", "type": "module", "main": "./dist/index.cjs", @@ -74,12 +74,12 @@ "test": "vitest run", "test:e2e": "vitest run --config vitest.e2e.config.ts", "dev": "tsx src/cli.ts", - "prepublishOnly": "npm run build" + "prepublishOnly": "node ../../scripts/check-cohort-ranges.mjs && npm run build" }, "dependencies": { "@scure/bip32": "^2.2.0", "@scure/bip39": "^2.2.0", - "@tangle-network/sandbox": "^0.9.5", + "@tangle-network/sandbox": ">=0.27.1 <0.28.0", "@tangle-network/tcloud-attestation": "workspace:^", "commander": "^14.0.3", "viem": "^2.48.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa4f9b6..fe53fc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,13 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - basic-ftp: 5.3.0 - fast-xml-parser: 5.7.0 - postcss: 8.5.10 - protobufjs: 7.5.5 - vite: 8.0.5 - importers: .: @@ -28,8 +21,8 @@ importers: specifier: ^2.2.0 version: 2.2.0 '@tangle-network/sandbox': - specifier: ^0.9.5 - version: 0.9.5(viem@2.48.4(typescript@5.9.3)(zod@4.4.3)) + specifier: '>=0.27.1 <0.28.0' + version: 0.27.1(viem@2.48.4(typescript@5.9.3)(zod@4.4.3)) '@tangle-network/tcloud-attestation': specifier: workspace:^ version: link:../tcloud-attestation @@ -45,7 +38,7 @@ importers: version: 22.19.15 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.0.0 version: 4.21.0 @@ -65,8 +58,8 @@ importers: specifier: ^0.34.49 version: 0.34.49 '@tangle-network/sandbox': - specifier: ^0.9.5 - version: 0.9.5(viem@2.48.4(typescript@5.9.3)(zod@4.4.3)) + specifier: '>=0.27.1 <0.28.0' + version: 0.27.1(viem@2.48.4(typescript@5.9.3)(zod@4.4.3)) '@tangle-network/tcloud': specifier: workspace:^ version: link:../tcloud @@ -76,7 +69,7 @@ importers: devDependencies: '@mariozechner/pi-coding-agent': specifier: ^0.70.2 - version: 0.70.2(ws@8.20.0)(zod@4.4.3) + version: 0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3) '@mariozechner/pi-tui': specifier: ^0.70.2 version: 0.70.2 @@ -85,7 +78,7 @@ importers: version: 22.19.15 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.0.0 version: 4.21.0 @@ -103,7 +96,7 @@ importers: version: 22.19.15 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -125,7 +118,7 @@ importers: version: 22.19.15 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.0.0 version: 4.21.0 @@ -727,20 +720,24 @@ packages: '@mariozechner/pi-agent-core@0.70.2': resolution: {integrity: sha512-g1hIdKyDwmQOoBGO0R4OhpemKeMENeK0vE5FJtuQKqEcsdCAkVBgZAK6aZUARYZVxMA718JS6WPLFWoddzjD7g==} engines: {node: '>=20.0.0'} + deprecated: please use @earendil-works/pi-agent-core instead going forward '@mariozechner/pi-ai@0.70.2': resolution: {integrity: sha512-+30LRPjXsXF+oI96DvGWMbdPGeqoLJvadh6UPev7wx2DzhC9FEqXkQcoMZ0usbCm7E9pl8ua8a9s/pQ5ikaUbg==} engines: {node: '>=20.0.0'} + deprecated: please use @earendil-works/pi-ai instead going forward hasBin: true '@mariozechner/pi-coding-agent@0.70.2': resolution: {integrity: sha512-asfNqV89HKAmKvJ1wENBY/UQMIf77kLtkzBrvXnMQV4YbH7D/6KT+VeVzPG6zm5PAZP2UtdLY9B9Cge7IxH37w==} engines: {node: '>=20.6.0'} + deprecated: please use @earendil-works/pi-coding-agent instead going forward hasBin: true '@mariozechner/pi-tui@0.70.2': resolution: {integrity: sha512-PtKC0NepnrYcqMx6MXkWTrBzC9tI62KeC6w940oT46lCbfvgmfqXciR15+9BZpxxc1H4jd3CMrKsmOPVeUqZ0A==} engines: {node: '>=20.0.0'} + deprecated: please use @earendil-works/pi-tui instead going forward '@mistralai/mistralai@2.2.1': resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==} @@ -1280,20 +1277,22 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@tangle-network/agent-core@0.3.8': - resolution: {integrity: sha512-bZfVpdiFjXbcQwSxSQABdtXEmUuapWChCcXrHkP6fAnwqEy9hWEfeZLlMZUy+1XaOfpTaMLUOEAwYhXfN018wQ==} - - '@tangle-network/agent-interface@0.13.0': - resolution: {integrity: sha512-CeTPGRLoXqpt0h+BCyFgZPkfU1zyRpWmqfD+85i/uk+uvbqxkfI+JprfKVf3tBsQuCgJPSjPt5qjdW8n3h2BVg==} + '@tangle-network/agent-core@0.9.4': + resolution: {integrity: sha512-2iSPCHPKI/9A4t1XXPTbpaJTzf3BkwNzXOJfb3M7AMjpIGR1SKNiRHKpDTTEUVbfx4tRny5qDDMhCnFmKgPsHA==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.30.0 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true - '@tangle-network/agent-interface@0.17.1': - resolution: {integrity: sha512-B7dRJTo0HSUtgBCB1VMwkTFYkLUaRr/4BcRglrQuGhGUwOzKv1RYyMejOVh5M3a5AagY9N79f7GYbjcA3UmnIA==} + '@tangle-network/agent-interface@1.0.0': + resolution: {integrity: sha512-rhXoscOE1TwkLvHg3tfqdumz++kcDRf7c5GRSTBnJkKLVypwvXB5yQJCB4aTUTrOCJxJMqowm2VPOmlTrUszOQ==} - '@tangle-network/sandbox@0.9.5': - resolution: {integrity: sha512-yvX2OX6uISBVnMQ+v6Upkesa3u8yj6BHxsfcS6p8Vze+M4WBpyhkwA+onzFHuo9rti557ItZn8yDu4a/klljvQ==} + '@tangle-network/sandbox@0.27.1': + resolution: {integrity: sha512-9mKAqsE3yMPWbiOEj8yEVGe2d6nSu9VFehhCT9q9jCORbMpLZ7Sl0/4yBtJoiSV5CkTay3VBlg25STMrS2qHmg==} peerDependencies: '@mastra/core': ^1.36.0 - '@modelcontextprotocol/sdk': ^1.29.0 + '@modelcontextprotocol/sdk': ^1.30.0 ai: ^6.0.175 openai: ^6.36.0 viem: ^2.0.0 @@ -1359,7 +1358,7 @@ packages: resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} peerDependencies: msw: ^2.4.9 - vite: 8.0.5 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true @@ -1618,8 +1617,8 @@ packages: fast-xml-builder@1.1.5: resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} - fast-xml-parser@5.7.0: - resolution: {integrity: sha512-MTcrUoRQ1GSQ9iG3QJzBGquYYYeA7piZaJoIWbPFGbRn6Jj6z7xgoAyi4DrZX4y2ZIQQBF59gc/zmvvejjgoFQ==} + fast-xml-parser@5.7.1: + resolution: {integrity: sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==} hasBin: true fd-slicer@1.1.0: @@ -2006,7 +2005,7 @@ packages: engines: {node: '>= 18'} peerDependencies: jiti: '>=1.21.0' - postcss: 8.5.10 + postcss: '>=8.0.9' tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: @@ -2110,6 +2109,15 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@5.0.0: + resolution: {integrity: sha512-vngmw3Rgn+o2arXNbnZaj5UtOEBuWBfvaI+Wc8GFfykIhA5/vdK9/Sp/XkLv63dykz2rxKDvKEHupF5P0FORcQ==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2199,7 +2207,7 @@ packages: peerDependencies: '@microsoft/api-extractor': ^7.36.0 '@swc/core': ^1 - postcss: 8.5.10 + postcss: ^8.4.12 typescript: '>=4.5.0' peerDependenciesMeta: '@microsoft/api-extractor': @@ -2322,7 +2330,7 @@ packages: '@vitest/ui': 4.1.5 happy-dom: '*' jsdom: '*' - vite: 8.0.5 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -2428,9 +2436,6 @@ packages: peerDependencies: zod: ^3.25.28 || ^4 - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -2861,7 +2866,7 @@ snapshots: '@aws-sdk/xml-builder@3.972.19': dependencies: '@smithy/types': 4.14.1 - fast-xml-parser: 5.7.0 + fast-xml-parser: 5.7.1 tslib: 2.8.1 '@aws/lambda-invoke-store@0.2.4': {} @@ -3050,9 +3055,9 @@ snapshots: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - '@google/genai@1.46.0': + '@google/genai@1.46.0(supports-color@7.2.0)': dependencies: - google-auth-library: 10.6.2 + google-auth-library: 10.6.2(supports-color@7.2.0) p-retry: 4.6.2 protobufjs: 7.5.5 ws: 8.20.0 @@ -3124,9 +3129,9 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mariozechner/pi-agent-core@0.70.2(ws@8.20.0)(zod@4.4.3)': + '@mariozechner/pi-agent-core@0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3)': dependencies: - '@mariozechner/pi-ai': 0.70.2(ws@8.20.0)(zod@4.4.3) + '@mariozechner/pi-ai': 0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3) typebox: 1.1.33 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -3137,16 +3142,16 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.70.2(ws@8.20.0)(zod@4.4.3)': + '@mariozechner/pi-ai@0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.90.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1037.0 - '@google/genai': 1.46.0 + '@google/genai': 1.46.0(supports-color@7.2.0) '@mistralai/mistralai': 2.2.1 chalk: 5.6.2 openai: 6.26.0(ws@8.20.0)(zod@4.4.3) partial-json: 0.1.7 - proxy-agent: 6.5.0 + proxy-agent: 6.5.0(supports-color@7.2.0) typebox: 1.1.33 undici: 7.24.6 zod-to-json-schema: 3.25.2(zod@4.4.3) @@ -3159,18 +3164,18 @@ snapshots: - ws - zod - '@mariozechner/pi-coding-agent@0.70.2(ws@8.20.0)(zod@4.4.3)': + '@mariozechner/pi-coding-agent@0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3)': dependencies: '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.70.2(ws@8.20.0)(zod@4.4.3) - '@mariozechner/pi-ai': 0.70.2(ws@8.20.0)(zod@4.4.3) + '@mariozechner/pi-agent-core': 0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3) + '@mariozechner/pi-ai': 0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3) '@mariozechner/pi-tui': 0.70.2 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cli-highlight: 2.1.11 diff: 8.0.4 - extract-zip: 2.0.1 - file-type: 21.3.4 + extract-zip: 2.0.1(supports-color@7.2.0) + file-type: 21.3.4(supports-color@7.2.0) glob: 13.0.6 hosted-git-info: 9.0.2 ignore: 7.0.5 @@ -3206,8 +3211,8 @@ snapshots: '@mistralai/mistralai@2.2.1': dependencies: ws: 8.20.0 - zod: 4.3.6 - zod-to-json-schema: 3.25.2(zod@4.3.6) + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -3745,29 +3750,28 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@tangle-network/agent-core@0.3.8': + '@tangle-network/agent-core@0.9.4': dependencies: - '@tangle-network/agent-interface': 0.17.1 + '@tangle-network/agent-interface': 1.0.0 zod: 4.4.3 - '@tangle-network/agent-interface@0.13.0': + '@tangle-network/agent-interface@1.0.0': dependencies: + '@noble/hashes': 1.8.0 + spdx-expression-parse: 5.0.0 zod: 4.4.3 - '@tangle-network/agent-interface@0.17.1': + '@tangle-network/sandbox@0.27.1(viem@2.48.4(typescript@5.9.3)(zod@4.4.3))': dependencies: + '@tangle-network/agent-core': 0.9.4 + '@tangle-network/agent-interface': 1.0.0 zod: 4.4.3 - - '@tangle-network/sandbox@0.9.5(viem@2.48.4(typescript@5.9.3)(zod@4.4.3))': - dependencies: - '@tangle-network/agent-core': 0.3.8 - '@tangle-network/agent-interface': 0.13.0 optionalDependencies: viem: 2.48.4(typescript@5.9.3)(zod@4.4.3) - '@tokenizer/inflate@0.4.1': + '@tokenizer/inflate@0.4.1(supports-color@7.2.0)': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) token-types: 6.1.2 transitivePeerDependencies: - supports-color @@ -3956,9 +3960,11 @@ snapshots: data-uri-to-buffer@6.0.2: {} - debug@4.4.3: + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 degenerator@5.0.1: dependencies: @@ -4081,9 +4087,9 @@ snapshots: extend@3.0.2: {} - extract-zip@2.0.1: + extract-zip@2.0.1(supports-color@7.2.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -4095,7 +4101,7 @@ snapshots: dependencies: path-expression-matcher: 1.5.0 - fast-xml-parser@5.7.0: + fast-xml-parser@5.7.1: dependencies: '@nodable/entities': 2.1.0 fast-xml-builder: 1.1.5 @@ -4115,9 +4121,9 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - file-type@21.3.4: + file-type@21.3.4(supports-color@7.2.0): dependencies: - '@tokenizer/inflate': 0.4.1 + '@tokenizer/inflate': 0.4.1(supports-color@7.2.0) strtok3: 10.3.5 token-types: 6.1.2 uint8array-extras: 1.5.0 @@ -4137,17 +4143,17 @@ snapshots: fsevents@2.3.3: optional: true - gaxios@7.1.4: + gaxios@7.1.4(supports-color@7.2.0): dependencies: extend: 3.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@7.2.0) node-fetch: 3.3.2 transitivePeerDependencies: - supports-color - gcp-metadata@8.1.2: + gcp-metadata@8.1.2(supports-color@7.2.0): dependencies: - gaxios: 7.1.4 + gaxios: 7.1.4(supports-color@7.2.0) google-logging-utils: 1.1.3 json-bigint: 1.0.0 transitivePeerDependencies: @@ -4165,11 +4171,11 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - get-uri@6.0.5: + get-uri@6.0.5(supports-color@7.2.0): dependencies: basic-ftp: 5.3.0 data-uri-to-buffer: 6.0.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -4179,12 +4185,12 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 - google-auth-library@10.6.2: + google-auth-library@10.6.2(supports-color@7.2.0): dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - gaxios: 7.1.4 - gcp-metadata: 8.1.2 + gaxios: 7.1.4(supports-color@7.2.0) + gcp-metadata: 8.1.2(supports-color@7.2.0) google-logging-utils: 1.1.3 jws: 4.0.1 transitivePeerDependencies: @@ -4204,17 +4210,17 @@ snapshots: dependencies: lru-cache: 11.2.7 - http-proxy-agent@7.0.2: + http-proxy-agent@7.0.2(supports-color@7.2.0): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@7.2.0): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -4411,16 +4417,16 @@ snapshots: '@types/retry': 0.12.0 retry: 0.13.1 - pac-proxy-agent@7.2.0: + pac-proxy-agent@7.2.0(supports-color@7.2.0): dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3 - get-uri: 6.0.5 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + get-uri: 6.0.5(supports-color@7.2.0) + http-proxy-agent: 7.0.2(supports-color@7.2.0) + https-proxy-agent: 7.0.6(supports-color@7.2.0) pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.5 + socks-proxy-agent: 8.0.5(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -4497,16 +4503,16 @@ snapshots: '@types/node': 22.19.15 long: 5.3.2 - proxy-agent@6.5.0: + proxy-agent@6.5.0(supports-color@7.2.0): dependencies: agent-base: 7.1.4 - debug: 4.4.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + http-proxy-agent: 7.0.2(supports-color@7.2.0) + https-proxy-agent: 7.0.6(supports-color@7.2.0) lru-cache: 7.18.3 - pac-proxy-agent: 7.2.0 + pac-proxy-agent: 7.2.0(supports-color@7.2.0) proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.5 + socks-proxy-agent: 8.0.5(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -4594,10 +4600,10 @@ snapshots: smart-buffer@4.2.0: {} - socks-proxy-agent@8.0.5: + socks-proxy-agent@8.0.5(supports-color@7.2.0): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -4614,6 +4620,15 @@ snapshots: source-map@0.7.6: {} + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@5.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + stackback@0.0.2: {} std-env@3.10.0: {} @@ -4691,13 +4706,13 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): + tsup@8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) esbuild: 0.27.7 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 @@ -4858,14 +4873,8 @@ snapshots: yoctocolors@2.1.2: {} - zod-to-json-schema@3.25.2(zod@4.3.6): - dependencies: - zod: 4.3.6 - zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: zod: 4.4.3 - zod@4.3.6: {} - zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 18ec407..1729778 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,6 @@ packages: - 'packages/*' +# First-party packages are cut and consumed inside this fleet, so a release-age +# hold on them would freeze this workspace behind its own cohort. +minimumReleaseAgeExclude: + - '@tangle-network/*' diff --git a/scripts/check-cohort-ranges.mjs b/scripts/check-cohort-ranges.mjs new file mode 100644 index 0000000..3c44691 --- /dev/null +++ b/scripts/check-cohort-ranges.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node +/** + * Fail a workspace manifest that would publish an exact first-party version pin. + * + * An exact pin names one version and refuses every other, so a consumer that + * already holds a later `@tangle-network/*` package installs a SECOND physical + * copy of the pinned one. Two copies of `@tangle-network/agent-interface` in a + * tree means two class identities and `instanceof` answering false across them. + * + * The range shape follows the depended-on package's own versioning: a caret + * from 1.0.0, where a minor is additive; the narrower `>=X.Y.Z 0) { + failures.push( + `${manifest.name} publishes exact first-party version pins, which duplicate the package for every consumer already holding a later one:\n${offenders + .map((line) => ` ${line}`) + .join('\n')}`, + ) + continue + } + process.stdout.write(`${manifest.name}@${manifest.version} declares first-party ranges only\n`) +} + +if (failures.length > 0) { + process.stderr.write( + `${failures.join('\n\n')}\nDeclare a range instead: a caret from 1.0.0, or ">=X.Y.Z Date: Sun, 16 Aug 2026 23:08:58 -0600 Subject: [PATCH 2/4] fix(deps): regenerate the lockfile with the pnpm version CI installs with CI installs with pnpm 9 and --frozen-lockfile. The lockfile carried no overrides block while the root manifest still declares pnpm.overrides for five packages, so pnpm 9 refused the frozen install with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH and the publish workflow could not reach build. Regenerating under pnpm 9.15.9 restores the overrides block and removes lockfile artifacts that only pnpm 10 and 11 emit. --- pnpm-lock.yaml | 159 +++++++++++++++++++++---------------------------- 1 file changed, 68 insertions(+), 91 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe53fc4..7fd0dbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,13 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + basic-ftp: 5.3.0 + fast-xml-parser: 5.7.0 + postcss: 8.5.10 + protobufjs: 7.5.5 + vite: 8.0.5 + importers: .: @@ -38,7 +45,7 @@ importers: version: 22.19.15 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.0.0 version: 4.21.0 @@ -69,7 +76,7 @@ importers: devDependencies: '@mariozechner/pi-coding-agent': specifier: ^0.70.2 - version: 0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3) + version: 0.70.2(ws@8.20.0)(zod@4.4.3) '@mariozechner/pi-tui': specifier: ^0.70.2 version: 0.70.2 @@ -78,7 +85,7 @@ importers: version: 22.19.15 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.0.0 version: 4.21.0 @@ -96,7 +103,7 @@ importers: version: 22.19.15 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -118,7 +125,7 @@ importers: version: 22.19.15 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.0.0 version: 4.21.0 @@ -667,35 +674,30 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@mariozechner/clipboard-linux-arm64-musl@0.3.3': resolution: {integrity: sha512-o1paj2+zmAQ/LaPS85XJCxhNowNQpxYM2cGY6pWvB5Kqmz6hZjl6CzDg5tbf1hZkn/Em6jpOaE2UtMxKdELBDA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@mariozechner/clipboard-linux-riscv64-gnu@0.3.3': resolution: {integrity: sha512-dkEhE4ekePJwMbBq9HP1//CFMNmDzA/iV9AXqBfvL5CWmmDIRXqh4A3YZt3tWO/HdMerX+xNCEiR7WiOsIG+UA==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@mariozechner/clipboard-linux-x64-gnu@0.3.3': resolution: {integrity: sha512-lT2yANtTLlEtFBIH3uGoRa/CQas/eBoLNi3qr9axQFoRgF4RGPSJ66yHOSnMECBneTIb1Iqv3UxokTfX27CdoQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@mariozechner/clipboard-linux-x64-musl@0.3.3': resolution: {integrity: sha512-saq/MCB0QHK/7ZZLjAZ0QkbY944dyjOsur8gneGCfMitt+GOiE1CU4OUipHC4b6x8UDY9bRLsR4aBaxu22OFPA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@mariozechner/clipboard-win32-arm64-msvc@0.3.3': resolution: {integrity: sha512-cGuvSj0/2X2w983yEcKw+i+r1EBej6ZZIN+fXG3eY2G/HaIQpbXpLvMxKyZ9LKtbZx+Z6q/gELEoSBMLML6BaQ==} @@ -846,42 +848,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} @@ -943,79 +939,66 @@ packages: resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.2': resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.2': resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.2': resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.2': resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.2': resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.2': resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.2': resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.2': resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.2': resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.2': resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.2': resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.2': resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.2': resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} @@ -1358,7 +1341,7 @@ packages: resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: 8.0.5 peerDependenciesMeta: msw: optional: true @@ -1617,8 +1600,8 @@ packages: fast-xml-builder@1.1.5: resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} - fast-xml-parser@5.7.1: - resolution: {integrity: sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==} + fast-xml-parser@5.7.0: + resolution: {integrity: sha512-MTcrUoRQ1GSQ9iG3QJzBGquYYYeA7piZaJoIWbPFGbRn6Jj6z7xgoAyi4DrZX4y2ZIQQBF59gc/zmvvejjgoFQ==} hasBin: true fd-slicer@1.1.0: @@ -1793,28 +1776,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2005,7 +1984,7 @@ packages: engines: {node: '>= 18'} peerDependencies: jiti: '>=1.21.0' - postcss: '>=8.0.9' + postcss: 8.5.10 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: @@ -2207,7 +2186,7 @@ packages: peerDependencies: '@microsoft/api-extractor': ^7.36.0 '@swc/core': ^1 - postcss: ^8.4.12 + postcss: 8.5.10 typescript: '>=4.5.0' peerDependenciesMeta: '@microsoft/api-extractor': @@ -2330,7 +2309,7 @@ packages: '@vitest/ui': 4.1.5 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: 8.0.5 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -2866,7 +2845,7 @@ snapshots: '@aws-sdk/xml-builder@3.972.19': dependencies: '@smithy/types': 4.14.1 - fast-xml-parser: 5.7.1 + fast-xml-parser: 5.7.0 tslib: 2.8.1 '@aws/lambda-invoke-store@0.2.4': {} @@ -3055,9 +3034,9 @@ snapshots: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - '@google/genai@1.46.0(supports-color@7.2.0)': + '@google/genai@1.46.0': dependencies: - google-auth-library: 10.6.2(supports-color@7.2.0) + google-auth-library: 10.6.2 p-retry: 4.6.2 protobufjs: 7.5.5 ws: 8.20.0 @@ -3129,9 +3108,9 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mariozechner/pi-agent-core@0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3)': + '@mariozechner/pi-agent-core@0.70.2(ws@8.20.0)(zod@4.4.3)': dependencies: - '@mariozechner/pi-ai': 0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3) + '@mariozechner/pi-ai': 0.70.2(ws@8.20.0)(zod@4.4.3) typebox: 1.1.33 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -3142,16 +3121,16 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3)': + '@mariozechner/pi-ai@0.70.2(ws@8.20.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.90.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1037.0 - '@google/genai': 1.46.0(supports-color@7.2.0) + '@google/genai': 1.46.0 '@mistralai/mistralai': 2.2.1 chalk: 5.6.2 openai: 6.26.0(ws@8.20.0)(zod@4.4.3) partial-json: 0.1.7 - proxy-agent: 6.5.0(supports-color@7.2.0) + proxy-agent: 6.5.0 typebox: 1.1.33 undici: 7.24.6 zod-to-json-schema: 3.25.2(zod@4.4.3) @@ -3164,18 +3143,18 @@ snapshots: - ws - zod - '@mariozechner/pi-coding-agent@0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3)': + '@mariozechner/pi-coding-agent@0.70.2(ws@8.20.0)(zod@4.4.3)': dependencies: '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3) - '@mariozechner/pi-ai': 0.70.2(supports-color@7.2.0)(ws@8.20.0)(zod@4.4.3) + '@mariozechner/pi-agent-core': 0.70.2(ws@8.20.0)(zod@4.4.3) + '@mariozechner/pi-ai': 0.70.2(ws@8.20.0)(zod@4.4.3) '@mariozechner/pi-tui': 0.70.2 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cli-highlight: 2.1.11 diff: 8.0.4 - extract-zip: 2.0.1(supports-color@7.2.0) - file-type: 21.3.4(supports-color@7.2.0) + extract-zip: 2.0.1 + file-type: 21.3.4 glob: 13.0.6 hosted-git-info: 9.0.2 ignore: 7.0.5 @@ -3769,9 +3748,9 @@ snapshots: optionalDependencies: viem: 2.48.4(typescript@5.9.3)(zod@4.4.3) - '@tokenizer/inflate@0.4.1(supports-color@7.2.0)': + '@tokenizer/inflate@0.4.1': dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 token-types: 6.1.2 transitivePeerDependencies: - supports-color @@ -3960,11 +3939,9 @@ snapshots: data-uri-to-buffer@6.0.2: {} - debug@4.4.3(supports-color@7.2.0): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 7.2.0 degenerator@5.0.1: dependencies: @@ -4087,9 +4064,9 @@ snapshots: extend@3.0.2: {} - extract-zip@2.0.1(supports-color@7.2.0): + extract-zip@2.0.1: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -4101,7 +4078,7 @@ snapshots: dependencies: path-expression-matcher: 1.5.0 - fast-xml-parser@5.7.1: + fast-xml-parser@5.7.0: dependencies: '@nodable/entities': 2.1.0 fast-xml-builder: 1.1.5 @@ -4121,9 +4098,9 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - file-type@21.3.4(supports-color@7.2.0): + file-type@21.3.4: dependencies: - '@tokenizer/inflate': 0.4.1(supports-color@7.2.0) + '@tokenizer/inflate': 0.4.1 strtok3: 10.3.5 token-types: 6.1.2 uint8array-extras: 1.5.0 @@ -4143,17 +4120,17 @@ snapshots: fsevents@2.3.3: optional: true - gaxios@7.1.4(supports-color@7.2.0): + gaxios@7.1.4: dependencies: extend: 3.0.2 - https-proxy-agent: 7.0.6(supports-color@7.2.0) + https-proxy-agent: 7.0.6 node-fetch: 3.3.2 transitivePeerDependencies: - supports-color - gcp-metadata@8.1.2(supports-color@7.2.0): + gcp-metadata@8.1.2: dependencies: - gaxios: 7.1.4(supports-color@7.2.0) + gaxios: 7.1.4 google-logging-utils: 1.1.3 json-bigint: 1.0.0 transitivePeerDependencies: @@ -4171,11 +4148,11 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - get-uri@6.0.5(supports-color@7.2.0): + get-uri@6.0.5: dependencies: basic-ftp: 5.3.0 data-uri-to-buffer: 6.0.2 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4185,12 +4162,12 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 - google-auth-library@10.6.2(supports-color@7.2.0): + google-auth-library@10.6.2: dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - gaxios: 7.1.4(supports-color@7.2.0) - gcp-metadata: 8.1.2(supports-color@7.2.0) + gaxios: 7.1.4 + gcp-metadata: 8.1.2 google-logging-utils: 1.1.3 jws: 4.0.1 transitivePeerDependencies: @@ -4210,17 +4187,17 @@ snapshots: dependencies: lru-cache: 11.2.7 - http-proxy-agent@7.0.2(supports-color@7.2.0): + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.6(supports-color@7.2.0): + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4417,16 +4394,16 @@ snapshots: '@types/retry': 0.12.0 retry: 0.13.1 - pac-proxy-agent@7.2.0(supports-color@7.2.0): + pac-proxy-agent@7.2.0: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3(supports-color@7.2.0) - get-uri: 6.0.5(supports-color@7.2.0) - http-proxy-agent: 7.0.2(supports-color@7.2.0) - https-proxy-agent: 7.0.6(supports-color@7.2.0) + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.5(supports-color@7.2.0) + socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color @@ -4503,16 +4480,16 @@ snapshots: '@types/node': 22.19.15 long: 5.3.2 - proxy-agent@6.5.0(supports-color@7.2.0): + proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@7.2.0) - http-proxy-agent: 7.0.2(supports-color@7.2.0) - https-proxy-agent: 7.0.6(supports-color@7.2.0) + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 lru-cache: 7.18.3 - pac-proxy-agent: 7.2.0(supports-color@7.2.0) + pac-proxy-agent: 7.2.0 proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.5(supports-color@7.2.0) + socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color @@ -4600,10 +4577,10 @@ snapshots: smart-buffer@4.2.0: {} - socks-proxy-agent@8.0.5(supports-color@7.2.0): + socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -4706,13 +4683,13 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(postcss@8.5.10)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): + tsup@8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 esbuild: 0.27.7 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 From 046dc60e4d7751f9957f85acf4e2ec6a30be09da Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Mon, 17 Aug 2026 01:04:54 -0600 Subject: [PATCH 3/4] fix(deps): close the exact-pin holes and gate every publishable package The guard matched only a bare version, so other spellings of the same exact pin passed: `=1.2.3`, a leading `v`, a whitespace-padded spec, and a `||` union of exact versions. `workspace:*`, a bare `workspace:`, and `workspace:` passed too, and pnpm rewrites each of those into that exact version at publish time. `workspace:^` and `workspace:~` stay ranges. tcloud-attestation and tcloud-relayer publish from the same release matrix but had no prepublishOnly hook, so a publish of either ran no gate at all. Both now run it, and a test asserts every publishable manifest keeps the hook. Route a cataloged profile on `PromptOptions.model`, the SDK's own per-turn selector, rather than hand-merging it into `backend.model`. The SDK keeps the transport's provider, apiKey and baseUrl, rejects an empty id, and throws on a conflict with an explicit transport model instead of preferring one silently. An inline profile and a cataloged id no longer travel together in one request, in either the session start or a per-turn override, because the wire defines no precedence between them. --- package.json | 1 + packages/tcloud-agent/src/agent-runner.ts | 39 +++--- .../tcloud-agent/tests/agent-runner.test.ts | 64 +++++++++- packages/tcloud-attestation/package.json | 1 + packages/tcloud-relayer/package.json | 1 + scripts/check-cohort-ranges.mjs | 111 ++++++++++++------ scripts/check-cohort-ranges.test.mjs | 101 ++++++++++++++++ 7 files changed, 268 insertions(+), 50 deletions(-) create mode 100644 scripts/check-cohort-ranges.test.mjs diff --git a/package.json b/package.json index 65a52cf..12c3bad 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "TypeScript SDK, CLI, agent, and relayer for Tangle AI Cloud", "scripts": { "build": "pnpm -r build", + "test:scripts": "node --test scripts/*.test.mjs", "dev": "pnpm --filter tcloud dev", "dev:relayer": "pnpm --filter tcloud-relayer dev" }, diff --git a/packages/tcloud-agent/src/agent-runner.ts b/packages/tcloud-agent/src/agent-runner.ts index 0e0eafa..0d7703e 100644 --- a/packages/tcloud-agent/src/agent-runner.ts +++ b/packages/tcloud-agent/src/agent-runner.ts @@ -310,15 +310,21 @@ class SandboxSdkAgentSessionTransport implements AgentSessionTransport { const sessionId = input.resume ?? this.options.sessionId const backend = this.options.backend ?? {} // `backend.profile` carries an inline profile definition only. A cataloged - // profile is an id, so it travels as the model selector — the same routing - // the bridge transport applies to a string profile. + // profile is an id, so it travels on `PromptOptions.model`: the SDK folds + // that into `backend.model.model`, keeps the transport's provider, apiKey + // and baseUrl, rejects an empty id, and throws on a conflict with an + // explicit transport-level model instead of silently preferring one. + // The two selectors never travel together — one names the profile, the + // other carries it inline, and the wire defines no precedence between them. + const { profile: _transportProfile, ...backendWithoutProfile } = backend + const selection: Pick = + typeof input.profile === 'string' + ? { model: input.profile, backend: backendWithoutProfile } + : { backend: { ...backend, profile: input.profile } } const promptOptions: PromptOptions = { sessionId, timeoutMs: this.options.timeoutMs, - backend: - typeof input.profile === 'string' - ? { ...backend, model: { ...(backend.model ?? {}), model: input.profile } } - : { ...backend, profile: input.profile }, + ...selection, context: input.workspace?.dir ? { workspaceDir: input.workspace.dir } : undefined, } @@ -720,14 +726,19 @@ function mergeSandbox( } function promptOptionsForTurn(base: PromptOptions, turn: AgentSessionChatOptions): PromptOptions { - return { - ...base, - sessionId: turn.sandbox?.sessionId ?? base.sessionId, - backend: { - ...(base.backend ?? {}), - ...(turn.sandbox?.agentProfile ? { profile: turn.sandbox.agentProfile } : {}), - }, - } + const sessionId = turn.sandbox?.sessionId ?? base.sessionId + const inlineProfile = turn.sandbox?.agentProfile + if (!inlineProfile) return { ...base, sessionId } + + // An inline profile for this turn replaces the session's cataloged selector. + // Sending both leaves the request with two selectors and no defined + // precedence, so drop the model id while keeping provider, apiKey and baseUrl. + const { model: _catalogedProfile, ...withoutCatalogedProfile } = base + const { model: _modelId, ...modelTransport } = base.backend?.model ?? {} + const backend = { ...(base.backend ?? {}), profile: inlineProfile } + if (Object.keys(modelTransport).length > 0) backend.model = modelTransport + else delete backend.model + return { ...withoutCatalogedProfile, sessionId, backend } } function lastUserText(messages: ChatMessage[]): string { diff --git a/packages/tcloud-agent/tests/agent-runner.test.ts b/packages/tcloud-agent/tests/agent-runner.test.ts index 423576a..9a75b3c 100644 --- a/packages/tcloud-agent/tests/agent-runner.test.ts +++ b/packages/tcloud-agent/tests/agent-runner.test.ts @@ -396,9 +396,66 @@ describe('Agent.run', () => { expect(prompts[0].message).toBe('hi') expect(prompts[0].options).toMatchObject({ sessionId: 'sdk-session', - backend: { model: { model: 'sf-proposer' } }, + model: 'sf-proposer', }) - expect((prompts[0].options as { backend: { profile?: unknown } }).backend.profile).toBeUndefined() + expect( + (prompts[0].options as { backend?: { profile?: unknown } }).backend?.profile, + ).toBeUndefined() + }) + + it('Sandbox SDK transport keeps the transport model transport beside a cataloged profile', async () => { + const prompts: Array<{ message: string; options: unknown }> = [] + const sandbox = { + async prompt(message: string, options: unknown) { + prompts.push({ message, options }) + return { success: true, response: 'sdk ok', durationMs: 12 } + }, + async *streamPrompt() { + throw new Error('streamPrompt should not be used when stream:false') + }, + } + await agent({ + transport: sandboxSdkTransport({ + sandbox: sandbox as any, + backend: { model: { provider: 'zai', apiKey: 'k' } } as any, + }), + profile: 'sf-proposer', + brief: 'hi', + stream: false, + }).run() + expect(prompts[0].options).toMatchObject({ + model: 'sf-proposer', + backend: { model: { provider: 'zai', apiKey: 'k' } }, + }) + expect( + (prompts[0].options as { backend?: { model?: { model?: unknown } } }).backend?.model?.model, + ).toBeUndefined() + }) + + it('Sandbox SDK transport drops a transport-level inline profile for a cataloged profile', async () => { + const prompts: Array<{ message: string; options: unknown }> = [] + const sandbox = { + async prompt(message: string, options: unknown) { + prompts.push({ message, options }) + return { success: true, response: 'sdk ok', durationMs: 12 } + }, + async *streamPrompt() { + throw new Error('streamPrompt should not be used when stream:false') + }, + } + await agent({ + transport: sandboxSdkTransport({ + sandbox: sandbox as any, + backend: { profile: { model: { default: 'kimi-k2' } } } as any, + }), + profile: 'sf-proposer', + brief: 'hi', + stream: false, + }).run() + expect(prompts[0].options).toMatchObject({ model: 'sf-proposer' }) + expect( + (prompts[0].options as { backend?: { profile?: unknown } }).backend?.profile, + ).toBeUndefined() }) it('Sandbox SDK transport sends an inline profile on backend.profile', async () => { @@ -421,8 +478,9 @@ describe('Agent.run', () => { }).run() expect(prompts[0].options).toMatchObject({ backend: { profile } }) expect( - (prompts[0].options as { backend: { model?: { model?: string } } }).backend.model?.model, + (prompts[0].options as { backend?: { model?: { model?: string } } }).backend?.model?.model, ).toBeUndefined() + expect((prompts[0].options as { model?: unknown }).model).toBeUndefined() }) it('forces non-streaming when usd budget is set so cost accounting can run', async () => { diff --git a/packages/tcloud-attestation/package.json b/packages/tcloud-attestation/package.json index 2746c57..8dddafe 100644 --- a/packages/tcloud-attestation/package.json +++ b/packages/tcloud-attestation/package.json @@ -20,6 +20,7 @@ }, "scripts": { "build": "tsup src/index.ts --format esm,cjs --dts --clean", + "prepublishOnly": "node ../../scripts/check-cohort-ranges.mjs && pnpm build", "test": "vitest run", "check-types": "tsc --noEmit" }, diff --git a/packages/tcloud-relayer/package.json b/packages/tcloud-relayer/package.json index 72f19f1..7fe8317 100644 --- a/packages/tcloud-relayer/package.json +++ b/packages/tcloud-relayer/package.json @@ -16,6 +16,7 @@ ], "scripts": { "build": "tsup src/index.ts --format esm --dts --clean", + "prepublishOnly": "node ../../scripts/check-cohort-ranges.mjs && pnpm build", "dev": "tsx src/index.ts", "start": "node dist/index.js" }, diff --git a/scripts/check-cohort-ranges.mjs b/scripts/check-cohort-ranges.mjs index 3c44691..b0c464d 100644 --- a/scripts/check-cohort-ranges.mjs +++ b/scripts/check-cohort-ranges.mjs @@ -15,44 +15,89 @@ import { readFileSync, readdirSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') -const packagesDir = join(repoRoot, 'packages') -const exactVersion = /^\d+\.\d+\.\d+(?:[-+].*)?$/ -const failures = [] - -for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { - if (!entry.isDirectory()) continue - const manifestPath = join(packagesDir, entry.name, 'package.json') - let manifest - try { - manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) - } catch { - continue +const FIRST_PARTY_SCOPE = '@tangle-network/' +const CHECKED_SECTIONS = ['dependencies', 'optionalDependencies', 'peerDependencies'] + +// npm accepts a leading `=` (with optional space) and a leading `v` on an exact +// version, and trims the spec before parsing. Each spelling names one version. +const EXACT_VERSION = /^(?:=\s*)?v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/ + +// pnpm rewrites a `workspace:` spec at publish time: `workspace:^` becomes +// `^version` and `workspace:~` becomes `~version`, but `workspace:*`, a bare +// `workspace:`, and `workspace:` all become that exact version. +const EXACT_WORKSPACE = /^workspace:\s*(?:\*|)$/ + +/** + * True when `spec` admits no version other than the ones it names, once npm or + * pnpm has rewritten it into the published manifest. + * + * npm reads `A || B` as the union of its parts, so a union of exact versions is + * still a closed set: no member of it can dedupe onto a consumer's later copy. + * A spec is safe only when at least one alternative is an open range. + */ +export function isExactPin(spec) { + if (typeof spec !== 'string') return false + const trimmed = spec.trim() + if (EXACT_WORKSPACE.test(trimmed)) return true + if (trimmed.startsWith('workspace:')) { + return EXACT_VERSION.test(trimmed.slice('workspace:'.length).trim()) } - if (manifest.private === true) continue + const alternatives = trimmed + .split('||') + .map((alternative) => alternative.trim()) + .filter((alternative) => alternative.length > 0) + if (alternatives.length === 0) return false + return alternatives.every((alternative) => EXACT_VERSION.test(alternative)) +} + +/** + * Return one `section.name = spec` line for every first-party exact pin. + * An empty array means the manifest is publishable under this policy. + */ +export function exactFirstPartyPins(manifest) { const offenders = [] - for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) { - for (const [name, spec] of Object.entries(manifest[section] ?? {})) { - if (!name.startsWith('@tangle-network/')) continue - if (typeof spec === 'string' && exactVersion.test(spec)) { - offenders.push(`${section}.${name} = ${spec}`) - } + for (const section of CHECKED_SECTIONS) { + for (const [name, spec] of Object.entries(manifest?.[section] ?? {})) { + if (!name.startsWith(FIRST_PARTY_SCOPE)) continue + if (isExactPin(spec)) offenders.push(`${section}.${name} = ${spec}`) } } - if (offenders.length > 0) { - failures.push( - `${manifest.name} publishes exact first-party version pins, which duplicate the package for every consumer already holding a later one:\n${offenders - .map((line) => ` ${line}`) - .join('\n')}`, + return offenders +} + +function main() { + const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') + const packagesDir = join(repoRoot, 'packages') + const failures = [] + + for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const manifestPath = join(packagesDir, entry.name, 'package.json') + let manifest + try { + manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) + } catch { + continue + } + if (manifest.private === true) continue + const offenders = exactFirstPartyPins(manifest) + if (offenders.length > 0) { + failures.push( + `${manifest.name} publishes exact first-party version pins, which duplicate the package for every consumer already holding a later one:\n${offenders + .map((line) => ` ${line}`) + .join('\n')}`, + ) + continue + } + process.stdout.write(`${manifest.name}@${manifest.version} declares first-party ranges only\n`) + } + + if (failures.length > 0) { + process.stderr.write( + `${failures.join('\n\n')}\nDeclare a range instead: a caret from 1.0.0, or ">=X.Y.Z 0) { - process.stderr.write( - `${failures.join('\n\n')}\nDeclare a range instead: a caret from 1.0.0, or ">=X.Y.Z =0.147.0 <0.148.0', + '>1.2.3', + '1.0.x', + '1.x', + '*', + 'latest', + 'workspace:^', + 'workspace:~', + 'npm:@tangle-network/other@^1.0.0', + '1.2.3 || ^2.0.0', +] + +test('isExactPin refuses every spelling of a closed version set', () => { + for (const spec of CLOSED) assert.equal(isExactPin(spec), true, spec) +}) + +test('isExactPin accepts a spec that admits more than a closed set', () => { + for (const spec of OPEN) assert.equal(isExactPin(spec), false, spec) +}) + +test('isExactPin accepts a non-string spec', () => { + assert.equal(isExactPin(undefined), false) + assert.equal(isExactPin({ version: '1.2.3' }), false) +}) + +test('exactFirstPartyPins reports one line per offending section and name', () => { + assert.deepEqual( + exactFirstPartyPins({ + dependencies: { '@tangle-network/sandbox': '0.27.1' }, + optionalDependencies: { '@tangle-network/agent-interface': '^1.0.0' }, + peerDependencies: { '@tangle-network/tcloud': 'workspace:*' }, + }), + [ + 'dependencies.@tangle-network/sandbox = 0.27.1', + 'peerDependencies.@tangle-network/tcloud = workspace:*', + ], + ) +}) + +test('exactFirstPartyPins ignores third-party exact pins', () => { + assert.deepEqual(exactFirstPartyPins({ dependencies: { viem: '2.48.4' } }), []) +}) + +test('exactFirstPartyPins reports nothing for a manifest with no checked sections', () => { + assert.deepEqual(exactFirstPartyPins({}), []) +}) + +test('every publishable workspace manifest passes the gate', () => { + const packagesDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'packages') + let checked = 0 + for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const manifest = JSON.parse( + readFileSync(join(packagesDir, entry.name, 'package.json'), 'utf8'), + ) + if (manifest.private === true) continue + assert.deepEqual(exactFirstPartyPins(manifest), [], manifest.name) + checked += 1 + } + assert.ok(checked > 0, 'found no publishable workspace manifest to check') +}) + +test('every publishable workspace manifest runs the gate before it publishes', () => { + const packagesDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'packages') + for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const manifest = JSON.parse( + readFileSync(join(packagesDir, entry.name, 'package.json'), 'utf8'), + ) + if (manifest.private === true) continue + assert.match( + manifest.scripts?.prepublishOnly ?? '', + /check-cohort-ranges\.mjs/, + `${manifest.name} publishes without running the cohort-range gate`, + ) + } +}) From d5799173a4913c6635dec94dbcff2124d1583cdf Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Mon, 17 Aug 2026 02:26:30 -0600 Subject: [PATCH 4/4] fix(agent): keep a model override across an inline-profile turn A per-turn inline profile stripped the transport's `backend.model.model` as well as the cataloged id. Only the cataloged id is ambiguous with an inline profile; a model override is a separate field the sandbox accepts beside a profile, and `start()` already keeps it for an inline profile. The two paths now agree, so the effective backend no longer changes between turns of one session. Cover the per-turn branch, which no test reached: the run loop never passes `sandbox` on chat, so the test drives the transport session directly. It fails against the stripping behavior and passes against this one. Extract the repeated fake sandbox into one recorder helper. --- packages/tcloud-agent/src/agent-runner.ts | 17 +-- .../tcloud-agent/tests/agent-runner.test.ts | 109 ++++++++++-------- 2 files changed, 73 insertions(+), 53 deletions(-) diff --git a/packages/tcloud-agent/src/agent-runner.ts b/packages/tcloud-agent/src/agent-runner.ts index 0d7703e..ee3cebf 100644 --- a/packages/tcloud-agent/src/agent-runner.ts +++ b/packages/tcloud-agent/src/agent-runner.ts @@ -730,15 +730,16 @@ function promptOptionsForTurn(base: PromptOptions, turn: AgentSessionChatOptions const inlineProfile = turn.sandbox?.agentProfile if (!inlineProfile) return { ...base, sessionId } - // An inline profile for this turn replaces the session's cataloged selector. - // Sending both leaves the request with two selectors and no defined - // precedence, so drop the model id while keeping provider, apiKey and baseUrl. + // An inline profile for this turn replaces the session's cataloged selector, + // so the cataloged id goes. A transport-level `backend.model` stays: the + // sandbox treats an inline profile and a model override as separate fields, + // which is what `start()` sends for an inline profile as well. const { model: _catalogedProfile, ...withoutCatalogedProfile } = base - const { model: _modelId, ...modelTransport } = base.backend?.model ?? {} - const backend = { ...(base.backend ?? {}), profile: inlineProfile } - if (Object.keys(modelTransport).length > 0) backend.model = modelTransport - else delete backend.model - return { ...withoutCatalogedProfile, sessionId, backend } + return { + ...withoutCatalogedProfile, + sessionId, + backend: { ...(base.backend ?? {}), profile: inlineProfile }, + } } function lastUserText(messages: ChatMessage[]): string { diff --git a/packages/tcloud-agent/tests/agent-runner.test.ts b/packages/tcloud-agent/tests/agent-runner.test.ts index 9a75b3c..ebb3c8c 100644 --- a/packages/tcloud-agent/tests/agent-runner.test.ts +++ b/packages/tcloud-agent/tests/agent-runner.test.ts @@ -126,6 +126,31 @@ async function collect(iter: AsyncIterable): Promise { // ---- run() tests (renamed from runUntil) ----------------------------------- +// ---- Fake sandbox ----------------------------------------------------------- +// +// Records every prompt the sandbox SDK transport sends. `streamPrompt` throws +// so a test that expects the non-streaming path fails loudly if it takes the +// other one. + +function makeRecordingSandbox() { + const prompts: Array<{ message: string; options: unknown }> = [] + const sandbox = { + async prompt(message: string, options: unknown) { + prompts.push({ message, options }) + return { + success: true, + response: 'sdk ok', + durationMs: 12, + usage: { inputTokens: 3, outputTokens: 4 }, + } + }, + async *streamPrompt() { + throw new Error('streamPrompt should not be used when stream:false') + }, + } + return { prompts, sandbox } +} + describe('Agent.run', () => { it('returns verified on iteration 1 when all criteria pass', async () => { const { client, calls } = makeFakeClient([makeCompletion('build passed, tsc ok')]) @@ -369,21 +394,7 @@ describe('Agent.run', () => { }) it('Sandbox SDK transport maps prompt responses into agent completions', async () => { - const prompts: Array<{ message: string; options: unknown }> = [] - const sandbox = { - async prompt(message: string, options: unknown) { - prompts.push({ message, options }) - return { - success: true, - response: 'sdk ok', - durationMs: 12, - usage: { inputTokens: 3, outputTokens: 4 }, - } - }, - async *streamPrompt() { - throw new Error('streamPrompt should not be used when stream:false') - }, - } + const { prompts, sandbox } = makeRecordingSandbox() const result = await agent({ transport: sandboxSdkTransport({ sandbox: sandbox as any }), profile: 'sf-proposer', @@ -404,16 +415,7 @@ describe('Agent.run', () => { }) it('Sandbox SDK transport keeps the transport model transport beside a cataloged profile', async () => { - const prompts: Array<{ message: string; options: unknown }> = [] - const sandbox = { - async prompt(message: string, options: unknown) { - prompts.push({ message, options }) - return { success: true, response: 'sdk ok', durationMs: 12 } - }, - async *streamPrompt() { - throw new Error('streamPrompt should not be used when stream:false') - }, - } + const { prompts, sandbox } = makeRecordingSandbox() await agent({ transport: sandboxSdkTransport({ sandbox: sandbox as any, @@ -433,16 +435,7 @@ describe('Agent.run', () => { }) it('Sandbox SDK transport drops a transport-level inline profile for a cataloged profile', async () => { - const prompts: Array<{ message: string; options: unknown }> = [] - const sandbox = { - async prompt(message: string, options: unknown) { - prompts.push({ message, options }) - return { success: true, response: 'sdk ok', durationMs: 12 } - }, - async *streamPrompt() { - throw new Error('streamPrompt should not be used when stream:false') - }, - } + const { prompts, sandbox } = makeRecordingSandbox() await agent({ transport: sandboxSdkTransport({ sandbox: sandbox as any, @@ -458,17 +451,43 @@ describe('Agent.run', () => { ).toBeUndefined() }) + it('Sandbox SDK transport swaps the cataloged id for a turn inline profile and keeps the model transport', async () => { + const { prompts, sandbox } = makeRecordingSandbox() + const turnProfile = { name: 'turn', prompt: 'be brief', model: { default: 'kimi-k2' } } + const session = sandboxSdkTransport({ + sandbox: sandbox as any, + backend: { model: { provider: 'zai', apiKey: 'k', model: 'claude-x' } } as any, + }).start({ profile: 'sf-proposer' }) + + await session.chat({ + messages: [{ role: 'user', content: 'hi' }], + sandbox: { agentProfile: turnProfile as any }, + }) + + // The cataloged id goes; the operator's explicit model override survives, + // which is what `start()` sends for an inline profile as well. + expect(prompts[0].options).toMatchObject({ + backend: { profile: turnProfile, model: { provider: 'zai', apiKey: 'k', model: 'claude-x' } }, + }) + expect((prompts[0].options as { model?: unknown }).model).toBeUndefined() + }) + + it('Sandbox SDK transport leaves a turn without an inline profile on the cataloged id', async () => { + const { prompts, sandbox } = makeRecordingSandbox() + const session = sandboxSdkTransport({ sandbox: sandbox as any }).start({ + profile: 'sf-proposer', + }) + + await session.chat({ messages: [{ role: 'user', content: 'hi' }] }) + + expect(prompts[0].options).toMatchObject({ model: 'sf-proposer' }) + expect( + (prompts[0].options as { backend?: { profile?: unknown } }).backend?.profile, + ).toBeUndefined() + }) + it('Sandbox SDK transport sends an inline profile on backend.profile', async () => { - const prompts: Array<{ message: string; options: unknown }> = [] - const sandbox = { - async prompt(message: string, options: unknown) { - prompts.push({ message, options }) - return { success: true, response: 'sdk ok', durationMs: 12 } - }, - async *streamPrompt() { - throw new Error('streamPrompt should not be used when stream:false') - }, - } + const { prompts, sandbox } = makeRecordingSandbox() const profile = { model: { default: 'kimi-k2' } } await agent({ transport: sandboxSdkTransport({ sandbox: sandbox as any }),