Skip to content

Commit 3e2d011

Browse files
committed
chore(gates): admit the published proxy package to the gates
R11 reads a published package's bundled workspace siblings from devDependencies, matching the public-package release rules. Fallow treats the proxy entries as published surface. The wire-compat ledger acks the fetch-based proxy declarations; their wire output is unchanged.
1 parent 75cdbe3 commit 3e2d011

6 files changed

Lines changed: 58 additions & 16 deletions

File tree

‎.fallowrc.json‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"src/sdk/selectors.ts",
1616
"src/sdk/finders.ts",
1717
"src/ai-sdk/index.ts",
18+
"packages/proxy/src/index.ts",
19+
"packages/proxy/src/node.ts",
20+
"packages/proxy/tsdown.config.ts",
1821
"src/bin.ts",
1922
"src/client/companion-tunnel.ts",
2023
"src/daemon.ts",
@@ -276,8 +279,8 @@
276279
"exports": ["*"]
277280
},
278281
{
279-
"comment": "Published package surface (see package.json#exports); consumed by SDK users, not internally.",
280-
"file": "{src/sdk/*.ts,src/ai-sdk/index.ts}",
282+
"comment": "Published package surfaces (see each package.json#exports); consumers may be external or reach them through workspace package exports Fallow does not follow.",
283+
"file": "{src/sdk/*.ts,src/ai-sdk/index.ts,packages/proxy/src/index.ts,packages/proxy/src/node.ts}",
281284
"exports": ["*"]
282285
},
283286
{
@@ -287,7 +290,7 @@
287290
},
288291
{
289292
"comment": "Tool config default exports, loaded by the tool rather than imported.",
290-
"file": "{oxlint.config.ts,tsdown.config.ts,vitest.mutation.config.ts,website/rspress.config.ts}",
293+
"file": "{oxlint.config.ts,tsdown.config.ts,packages/proxy/tsdown.config.ts,vitest.mutation.config.ts,website/rspress.config.ts}",
291294
"exports": ["default"]
292295
},
293296
{

‎scripts/layering/package-boundaries.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
rootExternalDependencyRanges,
2020
rootWorkspaceDependencyNames,
2121
specifierSites,
22+
workspacePackagesFromManifests,
2223
type WorkspacePackage,
2324
} from './package-boundaries.ts';
2425
import { listTrackedTypeScriptFiles } from './tracked-sources.ts';
@@ -33,6 +34,7 @@ const kernel: WorkspacePackage = {
3334
['@agent-device/kernel/device', 'packages/kernel/src/device.ts'],
3435
]),
3536
workspaceDependencies: new Set(),
37+
workspaceDependencyField: 'dependencies',
3638
externalDependencies: new Map(),
3739
};
3840

@@ -43,6 +45,7 @@ const contracts: WorkspacePackage = {
4345
['@agent-device/contracts/interaction', 'packages/contracts/src/interaction.ts'],
4446
]),
4547
workspaceDependencies: new Set(['@agent-device/kernel']),
48+
workspaceDependencyField: 'dependencies',
4649
externalDependencies: new Map(),
4750
};
4851

@@ -368,6 +371,34 @@ test('root workspace specifiers need a root workspace:* entry and an exported su
368371
assert.equal(checkRootSites(unknown, ALL, new Set([kernel.name])).length, 1);
369372
});
370373

374+
test('a published package declares its bundled workspace siblings as devDependencies', () => {
375+
const manifest = (name: string, isPrivate: boolean) =>
376+
JSON.stringify({
377+
name,
378+
private: isPrivate,
379+
exports: { '.': './src/index.ts' },
380+
devDependencies: { '@agent-device/kernel': 'workspace:*' },
381+
});
382+
const packages = workspacePackagesFromManifests(
383+
new Map([
384+
['packages/published/package.json', manifest('@agent-device/published', false)],
385+
['packages/internal/package.json', manifest('@agent-device/internal', true)],
386+
]),
387+
);
388+
const published = packages.find((pkg) => pkg.name === '@agent-device/published');
389+
const internal = packages.find((pkg) => pkg.name === '@agent-device/internal');
390+
assert.ok(published && internal);
391+
assert.deepEqual([...published.workspaceDependencies], ['@agent-device/kernel']);
392+
assert.deepEqual([...internal.workspaceDependencies], []);
393+
394+
const undeclared = specifierSites(
395+
'packages/published/src/index.ts',
396+
"import { g } from '@agent-device/contracts/interaction';",
397+
);
398+
const [violation] = checkPackageInternalSites(published, undeclared, [...ALL, published]);
399+
assert.match(violation?.message ?? '', /packages\/published\/package\.json devDependencies\.$/);
400+
});
401+
371402
test('the real tree parses, declares, and passes R11', () => {
372403
const packages = readWorkspacePackages(repoRoot);
373404
assert.ok(packages.length >= 1, 'expected at least the kernel package');

‎scripts/layering/package-boundaries.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export type WorkspacePackage = {
4949
exportTargets: ReadonlyMap<string, string>;
5050
/** Declared `workspace:*` dependencies on sibling internal packages. */
5151
workspaceDependencies: ReadonlySet<string>;
52+
/** Where siblings are declared: a published package bundles them, so it lists them as dev-only. */
53+
workspaceDependencyField: 'dependencies' | 'devDependencies';
5254
/** Non-workspace dependencies that the root build must externalize. */
5355
externalDependencies: ReadonlyMap<string, string>;
5456
};
@@ -102,6 +104,7 @@ export function workspacePackagesFromManifests(
102104
private?: boolean;
103105
exports?: Record<string, { default?: string } | string>;
104106
dependencies?: Record<string, string>;
107+
devDependencies?: Record<string, string>;
105108
};
106109
if (!manifest.name) continue;
107110
const exportTargets = new Map<string, string>();
@@ -113,8 +116,9 @@ export function workspacePackagesFromManifests(
113116
path.posix.join('packages', entry, path.posix.normalize(targetFile)),
114117
);
115118
}
119+
const workspaceDependencyField = manifest.private === true ? 'dependencies' : 'devDependencies';
116120
const workspaceDependencies = new Set(
117-
Object.entries(manifest.dependencies ?? {})
121+
Object.entries(manifest[workspaceDependencyField] ?? {})
118122
.filter(([, range]) => range.startsWith('workspace:'))
119123
.map(([name]) => name),
120124
);
@@ -128,6 +132,7 @@ export function workspacePackagesFromManifests(
128132
name: manifest.name,
129133
exportTargets,
130134
workspaceDependencies,
135+
workspaceDependencyField,
131136
externalDependencies,
132137
});
133138
}
@@ -190,7 +195,7 @@ export function checkPackageInternalSites(
190195
line: site.line,
191196
message:
192197
`${pkg.name} imports '${site.specifier}' without declaring "${name}": "workspace:*" ` +
193-
`in ${pkg.dir}/package.json dependencies.`,
198+
`in ${pkg.dir}/package.json ${pkg.workspaceDependencyField}.`,
194199
});
195200
}
196201
if (!target.exportTargets.has(site.specifier)) {

‎test/wire-compat/ledger.json‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,10 @@
185185
"packages/contracts/src/daemon-http.ts#DAEMON_HTTP_UPSTREAM_INSTANCE_HEADER": "sha256:4564c2ce437d30d7a823e3326f4375c0750ba8d42bec22b4d02e8eb7f7db2973",
186186
"packages/contracts/src/daemon-http.ts#DAEMON_HTTP_INSTANCE_MISMATCH_HEADER": "sha256:a197edaf7f9fa94a6aab269856aa852da3204cc586f5fd23198689eb11908b61",
187187
"src/daemon/server/http-instance-precondition.ts#refuseStaleDaemonInstance": "sha256:cf9b6f9dd95bb2593e4810c5b5939936b6d6358bf2614c7c234266cab1922d29",
188-
"packages/proxy/src/daemon-proxy.ts#refuseStaleProxyInstance": "sha256:82d39a1c241f1fb91c6bda2a55e9fde7b6d70db1a1a1496bef0eabf546ea706e",
189-
"packages/proxy/src/daemon-proxy.ts#sendInstanceMismatch": "sha256:d61ce46ef35d7aee9be4a41a79c84f372b05370097e3d8c8c109f06d494bfc55",
188+
"packages/proxy/src/daemon-proxy.ts#refuseStaleProxyInstance": "sha256:a5d802a666fd04a5c41a40bf3c96cbb4d05d6b449e950cf52098836df01df6ea",
189+
"packages/proxy/src/daemon-proxy.ts#sendInstanceMismatch": "sha256:7d0a889f8c7a8151a5ac7a37f52620a269b707144303cae7304825fca49e2a0d",
190190
"packages/contracts/src/daemon-http.ts#buildDaemonInstanceMismatchRpcResponse": "sha256:24e50130bbd0c4a21dfc51005cabf118525b9121f918719a2c16f73fcfcfbcf9",
191-
"packages/proxy/src/daemon-proxy.ts#buildUpstreamInstancePreconditionHeaders": "sha256:9f7a2f1a3f86fce2c939f03fce3c6db061c31d1a4bb8074a563e3c81196ecfbe",
191+
"packages/proxy/src/daemon-proxy.ts#buildUpstreamInstancePreconditionHeaders": "sha256:9f5d76a1f761d65fabd7244fc67295fed5934a544e86a6455451ef8520cf1615",
192192
"src/daemon-client/daemon-client-transport.ts#buildRemoteInstancePreconditionHeaders": "sha256:70241561c4070a8ebbdbbdb4ebcdbdfcb9543501cbb649a6d71cf041aee55a08",
193193
"src/daemon-client/daemon-client-transport.ts#isRemoteInstanceMismatchResponse": "sha256:7670f48fe0fc7344f8a000cd303e49788ec7ea9d1c729bcdc2af73ff02dae04d",
194194
"src/daemon-client/daemon-client-transport.ts#isRemoteInstanceMismatch": "sha256:6d4cfdd36bd44686d2a48990491faf63ba9117fb760f982fd72922c99be8daee"
@@ -401,18 +401,18 @@
401401
},
402402
{
403403
"declaration": "packages/proxy/src/daemon-proxy.ts#refuseStaleProxyInstance",
404-
"digest": "sha256:82d39a1c241f1fb91c6bda2a55e9fde7b6d70db1a1a1496bef0eabf546ea706e",
405-
"rationale": "#2650 adds conditional RPC refusal and retry only for peers that advertise complete instance IDs. Released peers advertise none, so new clients keep per-command health probes and send their unchanged RPC; older clients omit the new request precondition and parse existing responses as before."
404+
"digest": "sha256:a5d802a666fd04a5c41a40bf3c96cbb4d05d6b449e950cf52098836df01df6ea",
405+
"rationale": "Moved into @agent-device/proxy and rewritten over fetch Request/Response so the proxy can run on any transport. The 409 status, instance-mismatch header, JSON-RPC error body, and forwarded upstream precondition header are unchanged on the wire, so peers on both sides parse them as before."
406406
},
407407
{
408408
"declaration": "packages/proxy/src/daemon-proxy.ts#sendInstanceMismatch",
409-
"digest": "sha256:d61ce46ef35d7aee9be4a41a79c84f372b05370097e3d8c8c109f06d494bfc55",
410-
"rationale": "#2650 adds conditional RPC refusal and retry only for peers that advertise complete instance IDs. Released peers advertise none, so new clients keep per-command health probes and send their unchanged RPC; older clients omit the new request precondition and parse existing responses as before."
409+
"digest": "sha256:7d0a889f8c7a8151a5ac7a37f52620a269b707144303cae7304825fca49e2a0d",
410+
"rationale": "Moved into @agent-device/proxy and rewritten over fetch Request/Response so the proxy can run on any transport. The 409 status, instance-mismatch header, JSON-RPC error body, and forwarded upstream precondition header are unchanged on the wire, so peers on both sides parse them as before."
411411
},
412412
{
413413
"declaration": "packages/proxy/src/daemon-proxy.ts#buildUpstreamInstancePreconditionHeaders",
414-
"digest": "sha256:9f7a2f1a3f86fce2c939f03fce3c6db061c31d1a4bb8074a563e3c81196ecfbe",
415-
"rationale": "#2650 adds conditional RPC refusal and retry only for peers that advertise complete instance IDs. Released peers advertise none, so new clients keep per-command health probes and send their unchanged RPC; older clients omit the new request precondition and parse existing responses as before."
414+
"digest": "sha256:9f5d76a1f761d65fabd7244fc67295fed5934a544e86a6455451ef8520cf1615",
415+
"rationale": "Moved into @agent-device/proxy and rewritten over fetch Request/Response so the proxy can run on any transport. The 409 status, instance-mismatch header, JSON-RPC error body, and forwarded upstream precondition header are unchanged on the wire, so peers on both sides parse them as before."
416416
},
417417
{
418418
"declaration": "src/daemon-client/daemon-client-transport.ts#buildRemoteInstancePreconditionHeaders",

‎test/wire-compat/module-resolution.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const TS_GLOBALS = new Set([
4242
'Error',
4343
'Exclude',
4444
'Extract',
45+
'Headers',
4546
'Map',
4647
'NonNullable',
4748
'Omit',
@@ -54,7 +55,9 @@ const TS_GLOBALS = new Set([
5455
'ReadonlyArray',
5556
'Record',
5657
'RegExp',
58+
'Request',
5759
'Required',
60+
'Response',
5861
'ReturnType',
5962
'Set',
6063
'URL',

‎test/wire-compat/wire-mutations.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ const MUTATIONS: readonly WireMutation[] = [
6060
breakClass: 'instance refusal: the proxy drops the mismatch response header',
6161
file: 'packages/proxy/src/daemon-proxy.ts',
6262
name: 'sendInstanceMismatch',
63-
from: "res.setHeader(DAEMON_HTTP_INSTANCE_MISMATCH_HEADER, 'true');",
64-
to: '',
63+
from: "{ status: 409, headers: { [DAEMON_HTTP_INSTANCE_MISMATCH_HEADER]: 'true' } }",
64+
to: '{ status: 409 }',
6565
},
6666
{
6767
breakClass: 'instance refusal: the shared RPC error code changes',

0 commit comments

Comments
 (0)