@vellumai/cli 0.11.2 → 0.11.3-staging.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.
Files changed (97) hide show
  1. package/knip.json +1 -0
  2. package/node_modules/@vellumai/environments/package.json +2 -1
  3. package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
  4. package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
  5. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
  6. package/node_modules/@vellumai/environments/src/index.ts +17 -6
  7. package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
  8. package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +18 -0
  13. package/node_modules/@vellumai/ipc-server-utils/src/ipc-framing.ts +295 -0
  14. package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
  15. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +444 -0
  16. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +236 -0
  17. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  18. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/package.json +2 -1
  19. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
  20. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
  21. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
  22. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/index.ts +17 -6
  23. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
  24. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/seeds.ts +29 -0
  25. package/node_modules/@vellumai/local-mode/node_modules/nanoid/LICENSE +20 -0
  26. package/node_modules/@vellumai/local-mode/node_modules/nanoid/README.md +38 -0
  27. package/node_modules/@vellumai/local-mode/node_modules/nanoid/bin/nanoid.js +55 -0
  28. package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.browser.js +29 -0
  29. package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.d.ts +106 -0
  30. package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.js +47 -0
  31. package/node_modules/@vellumai/local-mode/node_modules/nanoid/nanoid.js +1 -0
  32. package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.d.ts +48 -0
  33. package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.js +21 -0
  34. package/node_modules/@vellumai/local-mode/node_modules/nanoid/package.json +46 -0
  35. package/node_modules/@vellumai/local-mode/node_modules/nanoid/url-alphabet/index.js +2 -0
  36. package/node_modules/@vellumai/local-mode/package.json +1 -0
  37. package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +63 -1
  38. package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +503 -0
  39. package/node_modules/@vellumai/local-mode/src/__tests__/guardian-token.test.ts +203 -0
  40. package/node_modules/@vellumai/local-mode/src/__tests__/loopback-auth.test.ts +46 -1
  41. package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +6 -4
  42. package/node_modules/@vellumai/local-mode/src/__tests__/pair.test.ts +621 -0
  43. package/node_modules/@vellumai/local-mode/src/__tests__/status.test.ts +5 -0
  44. package/node_modules/@vellumai/local-mode/src/__tests__/unpair.test.ts +232 -0
  45. package/node_modules/@vellumai/local-mode/src/config.ts +126 -20
  46. package/node_modules/@vellumai/local-mode/src/environment.ts +36 -15
  47. package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +313 -25
  48. package/node_modules/@vellumai/local-mode/src/guardian-token.ts +222 -13
  49. package/node_modules/@vellumai/local-mode/src/index.ts +35 -3
  50. package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +18 -0
  51. package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +72 -0
  52. package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +130 -0
  53. package/node_modules/@vellumai/local-mode/src/lockfile.ts +134 -45
  54. package/node_modules/@vellumai/local-mode/src/pair.ts +354 -0
  55. package/node_modules/@vellumai/local-mode/src/paths.ts +49 -0
  56. package/node_modules/@vellumai/local-mode/src/status.ts +10 -8
  57. package/node_modules/@vellumai/local-mode/src/unpair.ts +101 -0
  58. package/node_modules/@vellumai/local-mode/src/util.ts +34 -0
  59. package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  60. package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  61. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  62. package/package.json +3 -1
  63. package/src/__tests__/client-token.test.ts +26 -1
  64. package/src/__tests__/guardian-token.test.ts +4 -0
  65. package/src/__tests__/nginx-ingress-command.test.ts +176 -16
  66. package/src/__tests__/nginx-ingress.test.ts +1821 -101
  67. package/src/__tests__/pair.test.ts +104 -2
  68. package/src/__tests__/sleep.test.ts +7 -3
  69. package/src/__tests__/tailscale-tunnel.test.ts +0 -1
  70. package/src/__tests__/tunnel.test.ts +1099 -55
  71. package/src/__tests__/wake.test.ts +166 -99
  72. package/src/commands/client.ts +182 -27
  73. package/src/commands/connect/import.ts +35 -156
  74. package/src/commands/nginx-ingress.ts +87 -110
  75. package/src/commands/pair.ts +84 -21
  76. package/src/commands/tunnel.ts +196 -85
  77. package/src/commands/upgrade.ts +3 -2
  78. package/src/commands/wake.ts +5 -163
  79. package/src/index.ts +1 -1
  80. package/src/lib/__tests__/local-ces.test.ts +10 -1
  81. package/src/lib/cloudflare-tunnel.ts +3 -16
  82. package/src/lib/environments/__tests__/paths.test.ts +20 -1
  83. package/src/lib/environments/paths.ts +29 -41
  84. package/src/lib/environments/resolve.ts +7 -5
  85. package/src/lib/guardian-token.ts +22 -62
  86. package/src/lib/ingress-config.ts +25 -0
  87. package/src/lib/local.ts +77 -20
  88. package/src/lib/nginx-ingress.ts +523 -76
  89. package/src/lib/ngrok.ts +154 -61
  90. package/src/lib/orphan-detection.test.ts +167 -0
  91. package/src/lib/orphan-detection.ts +40 -0
  92. package/src/lib/pair.test.ts +87 -0
  93. package/src/lib/pair.ts +55 -0
  94. package/src/lib/platform-client.ts +8 -5
  95. package/src/lib/tailscale-tunnel.ts +7 -18
  96. package/src/lib/tunnel-edge.ts +114 -0
  97. package/src/lib/xdg-log.ts +2 -2
@@ -0,0 +1,203 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
+
6
+ import {
7
+ getGuardianAccessToken,
8
+ PAIRED_GUARDIAN_TARGET_MISMATCH_ERROR,
9
+ PAIRED_GUARDIAN_TOKEN_HOST_ONLY_ERROR,
10
+ saveGuardianToken,
11
+ type GuardianTokenData,
12
+ } from "../guardian-token";
13
+ import type { CliInvocation } from "../util";
14
+
15
+ // An invocation that would fail loudly if any tested branch spawned the CLI.
16
+ const invocation: CliInvocation = { command: "false", baseArgs: [] };
17
+
18
+ const FUTURE = new Date(Date.now() + 60 * 60 * 1000).toISOString();
19
+ const PAST = new Date(Date.now() - 60_000).toISOString();
20
+
21
+ function makeTokenData(over: Partial<GuardianTokenData>): GuardianTokenData {
22
+ return {
23
+ guardianPrincipalId: "principal",
24
+ accessToken: "access",
25
+ accessTokenExpiresAt: FUTURE,
26
+ refreshToken: "refresh",
27
+ refreshTokenExpiresAt: FUTURE,
28
+ refreshAfter: FUTURE,
29
+ isNew: false,
30
+ deviceId: "device",
31
+ leasedAt: new Date().toISOString(),
32
+ ...over,
33
+ };
34
+ }
35
+
36
+ describe("getGuardianAccessToken", () => {
37
+ let configDir: string;
38
+
39
+ beforeEach(() => {
40
+ configDir = fs.mkdtempSync(path.join(os.tmpdir(), "guardian-token-test-"));
41
+ });
42
+
43
+ afterEach(() => {
44
+ fs.rmSync(configDir, { recursive: true, force: true });
45
+ });
46
+
47
+ test("returns the stored token while the access token is fresh", async () => {
48
+ saveGuardianToken(configDir, "asst-1", makeTokenData({}));
49
+
50
+ expect(
51
+ await getGuardianAccessToken("asst-1", configDir, invocation, true),
52
+ ).toEqual({ ok: true, accessToken: "access" });
53
+ });
54
+
55
+ test("returns a structured 404 when no token file exists", async () => {
56
+ expect(
57
+ await getGuardianAccessToken("missing", configDir, invocation, true),
58
+ ).toEqual({ ok: false, status: 404, error: "Guardian token not found" });
59
+ });
60
+
61
+ test("expired refresh token yields hatch/wake guidance by default", async () => {
62
+ saveGuardianToken(
63
+ configDir,
64
+ "asst-1",
65
+ makeTokenData({ accessTokenExpiresAt: PAST, refreshTokenExpiresAt: PAST }),
66
+ );
67
+
68
+ const result = await getGuardianAccessToken(
69
+ "asst-1",
70
+ configDir,
71
+ invocation,
72
+ true,
73
+ );
74
+
75
+ expect(result).toEqual({
76
+ ok: false,
77
+ status: 401,
78
+ error: "Guardian token expired. Re-run `vellum hatch` or `vellum wake`.",
79
+ });
80
+ });
81
+
82
+ test("expired refresh token yields re-pair guidance for a paired entry", async () => {
83
+ saveGuardianToken(
84
+ configDir,
85
+ "asst-1",
86
+ makeTokenData({ accessTokenExpiresAt: PAST, refreshTokenExpiresAt: PAST }),
87
+ );
88
+
89
+ const result = await getGuardianAccessToken(
90
+ "asst-1",
91
+ configDir,
92
+ invocation,
93
+ true,
94
+ undefined,
95
+ {
96
+ paired: true,
97
+ pairedGatewayUrl: "https://gateway.example.com",
98
+ },
99
+ );
100
+
101
+ expect(result).toEqual({
102
+ ok: false,
103
+ status: 401,
104
+ error:
105
+ "Guardian token expired. Run `vellum pair` on the assistant's machine, then re-import it from the app's connect flow or with `vellum connect import`.",
106
+ });
107
+ });
108
+
109
+ test("an explicit paired: false keeps the hatch/wake guidance", async () => {
110
+ saveGuardianToken(
111
+ configDir,
112
+ "asst-1",
113
+ makeTokenData({ accessTokenExpiresAt: PAST, refreshTokenExpiresAt: PAST }),
114
+ );
115
+
116
+ const result = await getGuardianAccessToken(
117
+ "asst-1",
118
+ configDir,
119
+ invocation,
120
+ true,
121
+ undefined,
122
+ { paired: false },
123
+ );
124
+
125
+ expect(result.ok).toBe(false);
126
+ if (result.ok) {
127
+ return;
128
+ }
129
+ expect(result.error).toContain("vellum hatch");
130
+ });
131
+
132
+ test("returns a paired token only for its stored gateway target", async () => {
133
+ saveGuardianToken(
134
+ configDir,
135
+ "paired-1",
136
+ makeTokenData({ pairedGatewayUrl: "https://gateway.example.com" }),
137
+ );
138
+
139
+ expect(
140
+ await getGuardianAccessToken("paired-1", configDir, invocation, true),
141
+ ).toEqual({
142
+ ok: false,
143
+ status: 403,
144
+ error: PAIRED_GUARDIAN_TOKEN_HOST_ONLY_ERROR,
145
+ });
146
+ expect(
147
+ await getGuardianAccessToken(
148
+ "paired-1",
149
+ configDir,
150
+ invocation,
151
+ true,
152
+ undefined,
153
+ {
154
+ paired: true,
155
+ pairedGatewayUrl: "https://attacker.example.com",
156
+ },
157
+ ),
158
+ ).toEqual({
159
+ ok: false,
160
+ status: 403,
161
+ error: PAIRED_GUARDIAN_TARGET_MISMATCH_ERROR,
162
+ });
163
+ expect(
164
+ await getGuardianAccessToken(
165
+ "paired-1",
166
+ configDir,
167
+ invocation,
168
+ true,
169
+ undefined,
170
+ {
171
+ paired: true,
172
+ pairedGatewayUrl: "https://gateway.example.com",
173
+ },
174
+ ),
175
+ ).toEqual({ ok: true, accessToken: "access" });
176
+ });
177
+
178
+ test("binds a legacy paired token before returning it", async () => {
179
+ saveGuardianToken(configDir, "paired-1", makeTokenData({}));
180
+
181
+ expect(
182
+ await getGuardianAccessToken(
183
+ "paired-1",
184
+ configDir,
185
+ invocation,
186
+ true,
187
+ undefined,
188
+ {
189
+ paired: true,
190
+ pairedGatewayUrl: "https://gateway.example.com",
191
+ },
192
+ ),
193
+ ).toEqual({ ok: true, accessToken: "access" });
194
+
195
+ const stored = JSON.parse(
196
+ fs.readFileSync(
197
+ path.join(configDir, "assistants", "paired-1", "guardian-token.json"),
198
+ "utf-8",
199
+ ),
200
+ ) as GuardianTokenData;
201
+ expect(stored.pairedGatewayUrl).toBe("https://gateway.example.com");
202
+ });
203
+ });
@@ -3,7 +3,11 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { afterEach, describe, expect, test } from "bun:test";
5
5
 
6
- import { headerHostIsLoopback, originIsAllowed } from "../util";
6
+ import {
7
+ hasSameOriginCredentialProof,
8
+ headerHostIsLoopback,
9
+ originIsAllowed,
10
+ } from "../util";
7
11
  import { isActiveAssistant } from "../lockfile";
8
12
 
9
13
  describe("headerHostIsLoopback", () => {
@@ -41,6 +45,47 @@ describe("originIsAllowed", () => {
41
45
  });
42
46
  });
43
47
 
48
+ describe("hasSameOriginCredentialProof", () => {
49
+ test("requires an Origin or explicit same-origin Fetch Metadata", () => {
50
+ expect(
51
+ hasSameOriginCredentialProof("127.0.0.1:3000", undefined, undefined),
52
+ ).toBe(false);
53
+ expect(
54
+ hasSameOriginCredentialProof("127.0.0.1:3000", undefined, "same-origin"),
55
+ ).toBe(true);
56
+ expect(
57
+ hasSameOriginCredentialProof(undefined, undefined, "same-origin"),
58
+ ).toBe(false);
59
+ });
60
+
61
+ test("accepts only the exact loopback HTTP origin named by Host", () => {
62
+ expect(
63
+ hasSameOriginCredentialProof(
64
+ "127.0.0.1:3000",
65
+ "http://127.0.0.1:3000",
66
+ undefined,
67
+ ),
68
+ ).toBe(true);
69
+ expect(
70
+ hasSameOriginCredentialProof(
71
+ "127.0.0.1:3000",
72
+ "http://127.0.0.1:9999",
73
+ undefined,
74
+ ),
75
+ ).toBe(false);
76
+ });
77
+
78
+ test("rejects conflicting Fetch Metadata", () => {
79
+ expect(
80
+ hasSameOriginCredentialProof(
81
+ "127.0.0.1:3000",
82
+ "http://127.0.0.1:3000",
83
+ "same-site",
84
+ ),
85
+ ).toBe(false);
86
+ });
87
+ });
88
+
44
89
  describe("isActiveAssistant", () => {
45
90
  const tempDirs: string[] = [];
46
91
 
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * Enforces that the package:
9
9
  * 1. Imports only node builtins, its own relative modules, `@vellumai/environments`,
10
- * and `zod` (the lockfile contract's schema library) nothing else.
11
- * 2. Declares exactly those two runtime dependencies.
10
+ * `zod` (the lockfile contract's schema library), and `nanoid` (pair's
11
+ * fallback local-id generator); nothing else.
12
+ * 2. Declares exactly those runtime dependencies.
12
13
  * 3. Is marked `private`.
13
14
  */
14
15
 
@@ -19,7 +20,7 @@ import { join, resolve } from "node:path";
19
20
  const PACKAGE_ROOT = resolve(import.meta.dirname, "../..");
20
21
  const SRC_DIR = join(PACKAGE_ROOT, "src");
21
22
 
22
- const ALLOWED_PACKAGES = new Set(["@vellumai/environments", "zod"]);
23
+ const ALLOWED_PACKAGES = new Set(["@vellumai/environments", "zod", "nanoid"]);
23
24
 
24
25
  function collectSourceFiles(dir: string): string[] {
25
26
  const files: string[] = [];
@@ -91,13 +92,14 @@ describe("package boundary", () => {
91
92
  }
92
93
  });
93
94
 
94
- test("package.json declares it as private with only its environments and zod dependencies", () => {
95
+ test("package.json declares it as private with only its allowed dependencies", () => {
95
96
  const pkg = JSON.parse(
96
97
  readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"),
97
98
  );
98
99
  expect(pkg.private).toBe(true);
99
100
  expect(pkg.dependencies ?? {}).toEqual({
100
101
  "@vellumai/environments": "file:../environments",
102
+ nanoid: "5.1.7",
101
103
  zod: "4.3.6",
102
104
  });
103
105
  });