@hue-run/sdk 0.3.2 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CLI.md +270 -47
  2. package/ENVIRONMENTS.md +11 -1
  3. package/README.md +19 -3
  4. package/dist/client.d.ts +5 -5
  5. package/dist/client.js +13 -6
  6. package/dist/environment/tools.d.ts +6 -1
  7. package/dist/environment/tools.js +7 -1
  8. package/dist/environment/types.d.ts +6 -1
  9. package/dist/evals/simulation.d.ts +12 -4
  10. package/dist/evals/simulation.js +34 -24
  11. package/dist/evals.d.ts +1 -1
  12. package/dist/receipt.js +36 -8
  13. package/dist/setup/application.d.ts +74 -0
  14. package/dist/setup/application.js +766 -0
  15. package/dist/setup/backend.d.ts +229 -0
  16. package/dist/setup/backend.js +855 -0
  17. package/dist/setup/checkpoint.js +100 -30
  18. package/dist/setup/cli.js +20 -4
  19. package/dist/setup/configure.d.ts +13 -0
  20. package/dist/setup/configure.js +454 -0
  21. package/dist/setup/credential.d.ts +2 -0
  22. package/dist/setup/credential.js +9 -0
  23. package/dist/setup/detect.js +4 -1
  24. package/dist/setup/installation.d.ts +118 -0
  25. package/dist/setup/installation.js +605 -0
  26. package/dist/setup/lock.d.ts +2 -0
  27. package/dist/setup/lock.js +38 -0
  28. package/dist/setup/machine.d.ts +1 -10
  29. package/dist/setup/machine.js +8 -7
  30. package/dist/setup/render.d.ts +3 -1
  31. package/dist/setup/render.js +209 -6
  32. package/dist/setup/runner.d.ts +26 -76
  33. package/dist/setup/runner.js +320 -45
  34. package/dist/setup/socket.d.ts +7 -0
  35. package/dist/setup/socket.js +144 -0
  36. package/dist/setup/source.d.ts +9 -0
  37. package/dist/setup/source.js +269 -0
  38. package/dist/setup/types.d.ts +16 -9
  39. package/dist/setup/types.js +1 -1
  40. package/dist/setup.d.ts +6 -2
  41. package/dist/setup.js +3 -0
  42. package/dist/types.d.ts +24 -0
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +2 -1
  46. package/setup-events.schema.json +16 -9
@@ -1,14 +1,82 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
- import { chmod, lstat, mkdir, open, realpath, rename } from "node:fs/promises";
3
+ import { lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
6
  const MAX_CHECKPOINT_BYTES = 256 * 1024;
7
- const STEPS = ["detect-project", "configure-telemetry", "verify-receipt", "claim-project"];
7
+ const STEPS = [
8
+ "detect-project",
9
+ "install-runtime",
10
+ "configure-telemetry",
11
+ "verify-application-receipt",
12
+ "claim-project",
13
+ ];
14
+ const LEGACY_STEPS = [
15
+ "detect-project",
16
+ "configure-telemetry",
17
+ "verify-receipt",
18
+ "claim-project",
19
+ ];
8
20
  function isInside(parent, child) {
9
21
  const path = relative(parent, child);
10
22
  return path === "" || (!path.startsWith("..") && !isAbsolute(path));
11
23
  }
24
+ async function cleanupCheckpointTemporary(path) {
25
+ try {
26
+ await unlink(path);
27
+ }
28
+ catch (error) {
29
+ if (error.code !== "ENOENT")
30
+ throw error;
31
+ }
32
+ }
33
+ async function privateDirectory(root, project, enforcePermissions) {
34
+ const ancestors = [];
35
+ let current = root;
36
+ for (;;) {
37
+ ancestors.unshift(current);
38
+ const parent = dirname(current);
39
+ if (parent === current)
40
+ break;
41
+ current = parent;
42
+ }
43
+ for (const path of ancestors) {
44
+ let info;
45
+ try {
46
+ info = await lstat(path);
47
+ }
48
+ catch (error) {
49
+ if (error.code !== "ENOENT")
50
+ throw error;
51
+ // Earlier ancestors have all been inspected. Never recursively follow an
52
+ // unchecked ancestor into the project or a different owner's directory.
53
+ await mkdir(path, { mode: 0o700 });
54
+ info = await lstat(path);
55
+ }
56
+ if (info.isSymbolicLink() || !info.isDirectory())
57
+ throw new Error("Setup checkpoint ancestors must be directories without symlinks");
58
+ }
59
+ const actual = await realpath(root);
60
+ if (actual !== root || isInside(project, actual))
61
+ throw new Error("Setup checkpoints must be outside the project repository");
62
+ if (enforcePermissions) {
63
+ const handle = await open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
64
+ try {
65
+ await handle.chmod(0o700);
66
+ const opened = await handle.stat();
67
+ const current = await lstat(root);
68
+ if (!current.isDirectory() ||
69
+ current.isSymbolicLink() ||
70
+ current.dev !== opened.dev ||
71
+ current.ino !== opened.ino ||
72
+ (opened.mode & 0o077) !== 0)
73
+ throw new Error("Setup checkpoint directory changed during inspection");
74
+ }
75
+ finally {
76
+ await handle.close();
77
+ }
78
+ }
79
+ }
12
80
  export function defaultSetupStateDirectory(env = process.env) {
13
81
  if (platform() === "darwin")
14
82
  return join(homedir(), "Library", "Application Support", "Hue", "setup");
@@ -73,8 +141,8 @@ function validState(value, runId, projectRoot) {
73
141
  typeof plan === "object" &&
74
142
  !Array.isArray(plan) &&
75
143
  hasExactKeys(plan, ["steps", "mutatesProject", "backendRequired"]) &&
76
- JSON.stringify(plan.steps) === JSON.stringify(STEPS) &&
77
- plan.mutatesProject === false &&
144
+ [JSON.stringify(STEPS), JSON.stringify(LEGACY_STEPS)].includes(JSON.stringify(plan.steps)) &&
145
+ plan.mutatesProject === true &&
78
146
  plan.backendRequired === true);
79
147
  }
80
148
  export class FileSetupCheckpointAdapter {
@@ -94,24 +162,7 @@ export class FileSetupCheckpointAdapter {
94
162
  const project = await realpath(projectRoot);
95
163
  if (isInside(project, root))
96
164
  throw new Error("Setup checkpoints must be outside the project repository");
97
- let info;
98
- try {
99
- info = await lstat(root);
100
- }
101
- catch (error) {
102
- if (error.code !== "ENOENT")
103
- throw error;
104
- await mkdir(root, { recursive: true, mode: 0o700 });
105
- info = await lstat(root);
106
- }
107
- if (!info.isDirectory() || info.isSymbolicLink())
108
- throw new Error("Setup checkpoint directory must be private (mode 0700, no symlink)");
109
- if (this.enforcesPosixPermissions) {
110
- await chmod(root, 0o700);
111
- info = await lstat(root);
112
- if ((info.mode & 0o077) !== 0)
113
- throw new Error("Setup checkpoint directory must be private (mode 0700, no symlink)");
114
- }
165
+ await privateDirectory(root, project, this.enforcesPosixPermissions);
115
166
  return join(root, `${runId}.json`);
116
167
  }
117
168
  async load(runId, projectRoot) {
@@ -124,7 +175,7 @@ export class FileSetupCheckpointAdapter {
124
175
  throw new Error("Unsafe setup checkpoint symlink");
125
176
  }
126
177
  const noFollow = this.runtimePlatform === "win32" ? 0 : constants.O_NOFOLLOW;
127
- handle = await open(path, constants.O_RDONLY | noFollow);
178
+ handle = await open(path, constants.O_RDONLY | noFollow | constants.O_NONBLOCK);
128
179
  }
129
180
  catch (error) {
130
181
  if (error.code === "ENOENT")
@@ -148,6 +199,9 @@ export class FileSetupCheckpointAdapter {
148
199
  throw new Error("Setup checkpoint integrity check failed");
149
200
  if (!validState(state, runId, await realpath(projectRoot)))
150
201
  throw new Error("Setup checkpoint identity or shape does not match this project");
202
+ if (state.phase === "local-ready" &&
203
+ JSON.stringify(state.plan.steps) === JSON.stringify(LEGACY_STEPS))
204
+ return { ...state, plan: { ...state.plan, steps: [...STEPS] } };
151
205
  return state;
152
206
  }
153
207
  finally {
@@ -155,26 +209,42 @@ export class FileSetupCheckpointAdapter {
155
209
  }
156
210
  }
157
211
  async save(state) {
212
+ if (!validState(state, state.runId, await realpath(state.projectRoot)))
213
+ throw new Error("Invalid setup checkpoint state");
158
214
  const path = await this.pathFor(state.runId, state.projectRoot);
159
215
  const encoded = `${JSON.stringify({ state, digest: digest(state) })}\n`;
160
216
  if (Buffer.byteLength(encoded) > MAX_CHECKPOINT_BYTES)
161
217
  throw new Error("Setup checkpoint exceeds 256 KiB");
162
218
  const temporary = join(dirname(path), `.${state.runId}.${randomUUID()}.tmp`);
163
- const handle = await open(temporary, "wx", 0o600);
219
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
164
220
  try {
165
- await handle.writeFile(encoded);
166
- await handle.sync();
221
+ try {
222
+ await handle.writeFile(encoded);
223
+ await handle.sync();
224
+ }
225
+ finally {
226
+ await handle.close();
227
+ }
228
+ await this.pathFor(state.runId, state.projectRoot);
229
+ try {
230
+ const existing = await lstat(path);
231
+ if (existing.isSymbolicLink() || !existing.isFile())
232
+ throw new Error("Unsafe setup checkpoint target");
233
+ }
234
+ catch (error) {
235
+ if (error.code !== "ENOENT")
236
+ throw error;
237
+ }
238
+ await rename(temporary, path);
167
239
  }
168
240
  finally {
169
- await handle.close();
241
+ await cleanupCheckpointTemporary(temporary);
170
242
  }
171
- await rename(temporary, path);
172
243
  // Windows cannot open a directory as a file handle for fsync. The atomic rename and
173
244
  // per-user state directory still provide resumability there; POSIX additionally fsyncs
174
245
  // the containing directory so the rename survives a sudden interruption.
175
246
  if (this.runtimePlatform !== "win32") {
176
- await chmod(path, 0o600);
177
- const directory = await open(dirname(path), "r");
247
+ const directory = await open(dirname(path), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
178
248
  try {
179
249
  await directory.sync();
180
250
  }
package/dist/setup/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { realpath } from "node:fs/promises";
3
3
  import { parseArgs } from "node:util";
4
4
  import { FileSetupCheckpointAdapter, setupRunId } from "./checkpoint.js";
5
+ import { SetupBackendAdapter } from "./backend.js";
5
6
  import { detectSetupProject } from "./detect.js";
6
7
  import { renderHumanEvent, renderJsonlEvent, renderPlainEvent, selectSetupOutputMode, } from "./render.js";
7
8
  import { runSetup } from "./runner.js";
@@ -28,13 +29,15 @@ async function main() {
28
29
  agent: { type: "boolean", default: false },
29
30
  format: { type: "string" },
30
31
  project: { type: "string" },
32
+ origin: { type: "string" },
33
+ restart: { type: "boolean", default: false },
31
34
  help: { type: "boolean", short: "h", default: false },
32
35
  },
33
36
  });
34
37
  }
35
38
  catch {
36
39
  if (!agentRequested) {
37
- process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format plain|jsonl] [--project PATH]\n");
40
+ process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
38
41
  return 2;
39
42
  }
40
43
  const event = {
@@ -65,7 +68,7 @@ async function main() {
65
68
  process.stdout.write(`${renderJsonlEvent(event)}\n`);
66
69
  return 2;
67
70
  }
68
- process.stdout.write("Usage: hue <setup|resume|status|claim> [--agent|--format plain|jsonl] [--project PATH]\n");
71
+ process.stdout.write("Usage: hue <setup|resume|status|claim> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
69
72
  return 0;
70
73
  }
71
74
  const command = parsed.positionals[0];
@@ -75,7 +78,14 @@ async function main() {
75
78
  !commands.has(command) ||
76
79
  parsed.positionals.length !== 1 ||
77
80
  !validFormat ||
78
- (parsed.values.agent && format !== undefined && format !== "jsonl")) {
81
+ (parsed.values.agent && format !== undefined && format !== "jsonl") ||
82
+ (parsed.values.restart &&
83
+ (command !== "claim" ||
84
+ parsed.values.agent ||
85
+ format === "plain" ||
86
+ format === "jsonl" ||
87
+ !process.stdin.isTTY ||
88
+ !process.stdout.isTTY))) {
79
89
  if (parsed.values.agent) {
80
90
  const event = {
81
91
  contractVersion: SETUP_EVENT_CONTRACT_VERSION,
@@ -90,7 +100,7 @@ async function main() {
90
100
  process.stdout.write(`${renderJsonlEvent(event)}\n`);
91
101
  }
92
102
  else
93
- process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format plain|jsonl] [--project PATH]\n");
103
+ process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
94
104
  return 2;
95
105
  }
96
106
  const mode = selectSetupOutputMode({
@@ -107,6 +117,10 @@ async function main() {
107
117
  process.once("SIGTERM", interrupt);
108
118
  try {
109
119
  const root = await realpath(parsed.values.project ?? process.cwd());
120
+ const backend = new SetupBackendAdapter({
121
+ projectRoot: root,
122
+ ...(parsed.values.origin ? { origin: parsed.values.origin } : {}),
123
+ });
110
124
  await runSetup({
111
125
  command: command,
112
126
  mode,
@@ -114,6 +128,8 @@ async function main() {
114
128
  projectRoot: root,
115
129
  project: { detect: detectSetupProject },
116
130
  checkpoints: new FileSetupCheckpointAdapter(),
131
+ backend,
132
+ claimRestart: parsed.values.restart,
117
133
  signal: controller.signal,
118
134
  emit: (event) => {
119
135
  if (event.event === "run.completed" || event.event === "run.failed")
@@ -0,0 +1,13 @@
1
+ import type { SetupProjectDetection } from "./types.js";
2
+ import { type FileSetupInstallationStore, type SetupInstallationRecord } from "./installation.js";
3
+ /** A secret-free managed integration file created or replaced by setup. */
4
+ export interface SetupFileChange {
5
+ /** Project-relative managed file path. */
6
+ path: string;
7
+ /** Safe write performed during this invocation. */
8
+ change: "created" | "updated";
9
+ }
10
+ /** Refuses credential/config conflicts before setup makes a provisioning request. */
11
+ export declare function validateSetupConfiguration(store: FileSetupInstallationStore, record: Pick<SetupInstallationRecord, "managedFiles"> | undefined, project: SetupProjectDetection): Promise<void>;
12
+ /** Writes only secret-free, metadata-only integration modules and never executes project code. */
13
+ export declare function configureSetupProject(store: FileSetupInstallationStore, record: SetupInstallationRecord, project: SetupProjectDetection): Promise<SetupFileChange[]>;