@nurix/nustack 0.23.12 → 0.23.14

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 (72) hide show
  1. package/dist/commands/agent.js +1 -53
  2. package/dist/commands/check.js +73 -0
  3. package/dist/commands/deploy.js +2 -1
  4. package/dist/commands/doctor.js +2 -1
  5. package/dist/commands/harness.js +127 -48
  6. package/dist/commands/init.js +103 -3
  7. package/dist/commands/login.js +9 -2
  8. package/dist/commands/mcp.js +7 -0
  9. package/dist/commands/project-sync.js +2 -1
  10. package/dist/commands/read.js +19 -0
  11. package/dist/commands/records.js +10 -0
  12. package/dist/commands/review.js +3 -2
  13. package/dist/commands/sessions.js +4 -1
  14. package/dist/commands/sync.js +2 -1
  15. package/dist/index.js +4 -0
  16. package/dist/lib/agent/claudeHooks.js +4 -1
  17. package/dist/lib/agent/desktopDoor.js +55 -0
  18. package/dist/lib/auth_grant.js +15 -3
  19. package/dist/lib/auth_recovery.js +10 -3
  20. package/dist/lib/etna/manager.js +24 -6
  21. package/dist/lib/etna/profile.js +6 -0
  22. package/dist/lib/etna/reconcile.js +6 -0
  23. package/dist/lib/gate/bulkReadRelay.js +354 -0
  24. package/dist/lib/gate/hook.js +30 -3
  25. package/dist/lib/gate/outline.js +12 -0
  26. package/dist/lib/gate/settings.js +24 -1
  27. package/dist/lib/gate/tierOne.js +22 -0
  28. package/dist/lib/harness/codes.js +24 -0
  29. package/dist/lib/harness/hookCheck.js +325 -0
  30. package/dist/lib/harness/hookEntry.js +27 -0
  31. package/dist/lib/harness/legacyMount.js +231 -0
  32. package/dist/lib/harness/mount.js +199 -131
  33. package/dist/lib/harness/storeWaivers.js +43 -0
  34. package/dist/lib/harness/vendor.js +29 -0
  35. package/dist/lib/harness/versionsClient.js +54 -0
  36. package/dist/lib/local_store/database.js +18 -0
  37. package/dist/lib/local_store/routing.js +26 -0
  38. package/dist/lib/nustack_client.js +35 -2
  39. package/dist/lib/onboarding/attach_existing_git.js +14 -2
  40. package/dist/lib/onboarding/binding.js +46 -2
  41. package/dist/lib/onboarding/commit_trailer_hook.js +66 -0
  42. package/dist/lib/onboarding/manifest_writer.js +19 -3
  43. package/dist/lib/onboarding/session_hook.js +76 -0
  44. package/dist/lib/origin_guard.js +43 -0
  45. package/dist/lib/project_contract.js +104 -0
  46. package/dist/lib/records/door.js +26 -19
  47. package/dist/lib/records/drain.js +10 -4
  48. package/dist/lib/records/refusalLog.js +30 -1
  49. package/dist/lib/session-engine/supervisor.js +6 -18
  50. package/dist/lib/sessions/client.js +0 -12
  51. package/dist/program.js +27 -4
  52. package/dist/vendor/harness/assemble/runtimes.js +32 -0
  53. package/dist/vendor/harness/assemble/settingsMerge.js +136 -0
  54. package/dist/vendor/harness/digest/index.js +38 -0
  55. package/dist/vendor/harness/lib/findings.js +32 -0
  56. package/dist/vendor/harness/lib/frontmatter.js +79 -0
  57. package/dist/vendor/harness/lib/paths.js +31 -0
  58. package/dist/vendor/harness/mount/cache.js +65 -0
  59. package/dist/vendor/harness/mount/exclude.js +77 -0
  60. package/dist/vendor/harness/mount/files.js +38 -0
  61. package/dist/vendor/harness/mount/index.js +352 -0
  62. package/dist/vendor/harness/rules/changeRules.js +162 -0
  63. package/dist/vendor/harness/rules/context.js +38 -0
  64. package/dist/vendor/harness/rules/docsRoot.js +34 -0
  65. package/dist/vendor/harness/rules/index.js +109 -0
  66. package/dist/vendor/harness/rules/node.js +144 -0
  67. package/dist/vendor/harness/rules/outputGate.js +28 -0
  68. package/dist/vendor/harness/rules/ownership.js +178 -0
  69. package/dist/vendor/harness/rules/packageTable.js +113 -0
  70. package/dist/vendor/harness/rules/plans.js +96 -0
  71. package/dist/vendor/harness/rules/sessionWalls.js +31 -0
  72. package/package.json +5 -5
@@ -1,155 +1,223 @@
1
- import { execFileSync } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { readFileSync } from "node:fs";
4
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
5
- import { homedir } from "node:os";
1
+ import { existsSync, readFileSync } from "node:fs";
6
2
  import path from "node:path";
3
+ import { ensureAccessToken } from "../auth_grant.js";
4
+ import { getProject, resolveNuStackOrigin } from "../nustack_client.js";
5
+ import { isOriginMismatch } from "../origin_guard.js";
6
+ import { resolveProjectId } from "../project_context.js";
7
+ import { readProjectContract } from "../project_contract.js";
8
+ import { hasEtnaMountHook } from "../onboarding/session_hook.js";
9
+ import { HarnessError, HARNESS_CODES } from "./codes.js";
10
+ import { handOverLegacyMount, mountLegacyManifest, readLegacyMountRecord, readLegacyMountRecordSync } from "./legacyMount.js";
11
+ import { loadHarness } from "./vendor.js";
12
+ import { fetchVersionManifest, fetchVersionObject, resolveProjectVersion, VersionsRouteError } from "./versionsClient.js";
7
13
  export const MOUNT_STATE_PATH = ".claude/state/mount.json";
8
- const IGNORE_START = "# etna:mount:start";
9
- const IGNORE_END = "# etna:mount:end";
10
- const ETNA_MOUNT_SPECIFIER = "@nurix/etna/mount";
11
- const sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
12
- export function defaultCacheDir(env = process.env) {
13
- return env.ETNA_CACHE_DIR?.trim() || path.join(homedir(), ".etna", "cache");
14
- }
15
- export function isMountedPath(file) {
16
- if (file.policy)
17
- return file.policy === "mount";
18
- const contract = ["CLAUDE.md", "AGENTS.md", ".claude/harness.json", ".claude/settings.json", ".claude/hooks/mount.mjs", ".codex/hooks.json"];
19
- if (contract.includes(file.path))
20
- return false;
21
- return file.path.startsWith(".claude/") || file.path.startsWith(".agents/") || file.path.startsWith(".codex/");
22
- }
23
- async function readIfPresent(file) {
24
- try {
25
- return await readFile(file);
26
- }
27
- catch {
14
+ const DIGEST_REFUSALS = new Set(["mount.digest_mismatch", "mount.expected_digest_mismatch", "mount.object_mismatch", "mount.cache_corrupt", "mount.bad_sha256"]);
15
+ function stateOf(record) {
16
+ if (!record || typeof record.version !== "string" || typeof record.digest !== "string")
28
17
  return null;
29
- }
18
+ const source = record.source === "network" || record.source === "cache" || record.source === "legacy" ? record.source : null;
19
+ return {
20
+ version: record.version,
21
+ digest: record.digest,
22
+ mountedAt: typeof record.mountedAt === "string" ? record.mountedAt : "",
23
+ source,
24
+ origin: typeof record.origin === "string" ? record.origin : null,
25
+ };
30
26
  }
31
27
  export async function readMountState(root) {
32
- return parseMountState(await readIfPresent(path.join(root, MOUNT_STATE_PATH)));
28
+ const { readMountRecord } = await loadHarness("mount");
29
+ return stateOf(readMountRecord(root)) ?? stateOf((await readLegacyMountRecord(root)));
33
30
  }
34
31
  export function readMountStateSync(root) {
32
+ const file = path.join(root, MOUNT_STATE_PATH);
33
+ let record = null;
34
+ if (existsSync(file)) {
35
+ try {
36
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
37
+ record = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
38
+ }
39
+ catch {
40
+ record = null;
41
+ }
42
+ }
43
+ return stateOf(record) ?? stateOf(readLegacyMountRecordSync(root));
44
+ }
45
+ function coded(code, message) {
46
+ return Object.assign(new Error(message), { code });
47
+ }
48
+ export async function readSummary(root, projectId, globals) {
49
+ let baseUrl;
35
50
  try {
36
- return parseMountState(readFileSync(path.join(root, MOUNT_STATE_PATH)));
51
+ baseUrl = resolveNuStackOrigin(globals, root).url;
37
52
  }
38
- catch {
39
- return null;
53
+ catch (error) {
54
+ if (isOriginMismatch(error))
55
+ return { kind: "offline", reason: error.message, guardRefusal: error, baseUrl: null };
56
+ throw error;
40
57
  }
41
- }
42
- function parseMountState(raw) {
43
- if (!raw)
44
- return null;
58
+ let token;
45
59
  try {
46
- const parsed = JSON.parse(raw.toString("utf8"));
47
- if (typeof parsed.version !== "string" || typeof parsed.digest !== "string")
48
- return null;
49
- return { version: parsed.version, digest: parsed.digest, mountedAt: typeof parsed.mountedAt === "string" ? parsed.mountedAt : "", ...(typeof parsed.manifestUrl === "string" ? { manifestUrl: parsed.manifestUrl } : {}) };
60
+ token = await ensureAccessToken(baseUrl);
50
61
  }
51
- catch {
52
- return null;
62
+ catch (error) {
63
+ if (isOriginMismatch(error))
64
+ return { kind: "offline", reason: error.message, guardRefusal: error, baseUrl };
65
+ return { kind: "offline", reason: "this machine's sign-in could not be read", guardRefusal: null, baseUrl };
66
+ }
67
+ if (!token)
68
+ return { kind: "offline", reason: "this machine is not signed in", guardRefusal: null, baseUrl };
69
+ try {
70
+ const summary = await getProject(baseUrl, token.accessToken, projectId);
71
+ return { kind: "summary", summary, baseUrl, accessToken: token.accessToken };
72
+ }
73
+ catch (error) {
74
+ if (isOriginMismatch(error))
75
+ return { kind: "offline", reason: error.message, guardRefusal: error, baseUrl };
76
+ const code = error.code;
77
+ if (typeof code === "string" && code !== "UNEXPECTED" && code !== "SIGNED_OUT")
78
+ throw error;
79
+ return { kind: "offline", reason: `the Project summary could not be read (${error instanceof Error ? error.message : String(error)})`, guardRefusal: null, baseUrl };
53
80
  }
54
81
  }
55
- async function fetchBytes(fetchImpl, url) {
56
- const response = await fetchImpl(url);
57
- if (!response.ok)
58
- throw new Error(`${url} answered ${response.status}`);
59
- return Buffer.from(await response.arrayBuffer());
60
- }
61
- function objectUrlOf(manifestUrl, key) {
62
- return `${new URL(manifestUrl).origin}/${key.replace(/^\/+/, "")}`;
82
+ export function isProjectVersionSummary(summary) {
83
+ return Boolean(summary.harness?.digest || summary.harness?.versionManifestPath);
63
84
  }
64
- async function writeAtomic(target, bytes) {
65
- await mkdir(path.dirname(target), { recursive: true });
66
- const staging = `${target}.mount-${process.pid}`;
67
- await writeFile(staging, bytes);
68
- await rename(staging, target);
85
+ export async function projectOf(root, flag, contractProjectId) {
86
+ if (flag?.trim())
87
+ return flag.trim();
88
+ if (contractProjectId)
89
+ return contractProjectId;
90
+ const resolved = await resolveProjectId(root, {});
91
+ if (!resolved.ok)
92
+ throw coded("PROJECT_CONTEXT_MISSING", resolved.message);
93
+ return resolved.projectId;
69
94
  }
70
- function ignoredNamesOf(files) {
71
- const names = new Set([".claude/state/"]);
72
- for (const file of files) {
73
- const parts = file.path.split("/");
74
- if (parts[0] === ".agents")
75
- names.add(".agents/skills/");
76
- else if (parts[0] === ".codex")
77
- names.add(parts.length > 2 ? `.codex/${parts[1]}/` : `.codex/${parts[1]}`);
78
- else if (parts.length > 3)
79
- names.add(`${parts.slice(0, 3).join("/")}/`);
80
- else
81
- names.add(file.path);
95
+ function outcomeOf(projectId, result, lines, offlineCode) {
96
+ if (result.status === "mounted" || result.status === "current") {
97
+ const kept = result.findings.filter((finding) => finding.code === "mount.kept_edited" || finding.code === "mount.contract_edited");
98
+ return {
99
+ projectId,
100
+ status: result.status,
101
+ source: result.source ?? "network",
102
+ version: result.semver,
103
+ digest: result.digest,
104
+ runtime: result.runtime,
105
+ written: result.written,
106
+ kept: result.kept,
107
+ removed: result.removed,
108
+ lines: [...lines, result.line, ...kept.map((finding) => finding.message)],
109
+ };
82
110
  }
83
- return [...names].sort();
111
+ const finding = result.findings[0];
112
+ if (result.status === "refused" && finding && DIGEST_REFUSALS.has(finding.code))
113
+ throw new HarnessError(HARNESS_CODES.HARNESS_DIGEST_MISMATCH, result.line);
114
+ if (result.status === "unavailable" && (!finding || finding.code === "mount.no_route"))
115
+ throw new HarnessError(offlineCode, result.line);
116
+ throw new HarnessError(HARNESS_CODES.HARNESS_MOUNT_FAILED, result.line);
84
117
  }
85
- async function writeExcludeBlock(root, files) {
86
- let excludePath;
87
- try {
88
- excludePath = execFileSync("git", ["rev-parse", "--git-path", "info/exclude"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
118
+ async function recordedFiles(root, runtime) {
119
+ const { readMountRecord } = await loadHarness("mount");
120
+ return new Set((readMountRecord(root)?.runtimes?.[runtime]?.files ?? []).map((file) => file.path));
121
+ }
122
+ async function handOver(root, runtime, outcome) {
123
+ const handed = await handOverLegacyMount(root, await recordedFiles(root, runtime));
124
+ if (!handed)
125
+ return outcome;
126
+ const lines = [...outcome.lines, ...handed.editedKept.map((file) => `kept ${file}: the legacy harness wrote it and it has been edited since`)];
127
+ return { ...outcome, removed: [...outcome.removed, ...handed.removed], lines };
128
+ }
129
+ export async function mountProjectHarness(root, options) {
130
+ const log = options.log ?? ((line) => process.stderr.write(`${line}\n`));
131
+ const report = (outcome) => {
132
+ for (const line of outcome.lines)
133
+ log(line);
134
+ return outcome;
135
+ };
136
+ const runtime = options.runtime ?? "claude-code";
137
+ if (await hasEtnaMountHook(root)) {
138
+ throw new HarnessError(HARNESS_CODES.ETNA_MOUNT_PRESENT, "This checkout's .claude/settings.json runs etna's mount hook (.claude/hooks/mount.mjs), and two mounters on one checkout erase each other's record. Remove etna's SessionStart entry, then run `nustack init`.");
89
139
  }
90
- catch {
91
- return;
140
+ const contract = await readProjectContract(root);
141
+ const projectId = await projectOf(root, options.projectId, contract.projectId);
142
+ const lines = [];
143
+ let harness = contract.harness;
144
+ if (!harness) {
145
+ harness = { track: "latest" };
146
+ if (contract.contractPresent)
147
+ lines.push("This contract names no harness version, so the newest is mounted; run `nustack init` to record one.");
92
148
  }
93
- const absolute = path.isAbsolute(excludePath) ? excludePath : path.join(root, excludePath);
94
- const held = (await readIfPresent(absolute))?.toString("utf8") ?? "";
95
- const start = held.indexOf(IGNORE_START);
96
- const end = held.indexOf(IGNORE_END);
97
- const outside = start >= 0 && end > start ? held.slice(0, start) + held.slice(end + IGNORE_END.length).replace(/^\n/, "") : held;
98
- const block = [IGNORE_START, ...ignoredNamesOf(files), IGNORE_END].join("\n");
99
- await writeAtomic(absolute, `${outside.replace(/\n*$/, outside ? "\n" : "")}${block}\n`);
100
- }
101
- export async function mountWithAdapter(root, manifestUrl, options = {}) {
102
- const fetchImpl = options.fetchImpl ?? fetch;
103
- const cacheDir = options.cacheDir ?? defaultCacheDir();
104
- const manifestBytes = await fetchBytes(fetchImpl, manifestUrl);
105
- const digest = sha256Of(manifestBytes);
106
- const manifest = JSON.parse(manifestBytes.toString("utf8"));
107
- if (!manifest || !Array.isArray(manifest.files))
108
- throw new Error(`the manifest at ${manifestUrl} lists no files`);
109
- const version = String(manifest.version ?? "");
110
- const prior = await readMountState(root);
111
- const mounted = manifest.files.filter(isMountedPath);
112
- let written = 0;
113
- let kept = 0;
114
- for (const file of mounted) {
115
- if (file.path.split("/").includes("..") || path.isAbsolute(file.path))
116
- throw new Error(`the manifest names an unsafe path: ${file.path}`);
117
- const target = path.join(root, file.path);
118
- const held = await readIfPresent(target);
119
- if (held && file.sha256 && sha256Of(held) === file.sha256) {
120
- kept += 1;
121
- continue;
122
- }
123
- const cached = file.sha256 ? await readIfPresent(path.join(cacheDir, "objects", file.sha256)) : null;
124
- let bytes = cached && sha256Of(cached) === file.sha256 ? cached : null;
125
- if (!bytes) {
126
- bytes = await fetchBytes(fetchImpl, objectUrlOf(manifestUrl, file.key));
127
- if (file.sha256 && sha256Of(bytes) !== file.sha256)
128
- throw new Error(`the object for ${file.path} does not match its sha256`);
129
- if (file.sha256)
130
- await writeAtomic(path.join(cacheDir, "objects", file.sha256), bytes);
131
- }
132
- await writeAtomic(target, bytes);
133
- written += 1;
149
+ const pinned = "digest" in harness ? harness : null;
150
+ const { mountVersion, defaultHarnessCacheDir } = await loadHarness("mount");
151
+ const common = { root, cacheDir: defaultHarnessCacheDir(), runtime, projectId, writeContract: options.writeContract };
152
+ const noFetch = async () => {
153
+ throw new Error("a cached mount fetches nothing");
154
+ };
155
+ const fromCache = async (reason, guardRefusal, origin) => {
156
+ const result = await mountVersion({ ...common, manifest: null, fetchObject: noFetch, expectedDigest: pinned?.digest ?? null, recordExtras: origin ? { origin } : {} });
157
+ if (guardRefusal && result.status !== "mounted" && result.status !== "current")
158
+ throw new HarnessError(HARNESS_CODES.ORIGIN_MISMATCH, guardRefusal.message);
159
+ const notice = guardRefusal ? `harness taken from the cache, the origin was refused: ${guardRefusal.message}` : `harness taken from the cache: ${reason}`;
160
+ return handOver(root, runtime, outcomeOf(projectId, result, [...lines, notice], HARNESS_CODES.HARNESS_OFFLINE_NO_CACHE));
161
+ };
162
+ const read = await readSummary(root, projectId, options.globals ?? {});
163
+ if (read.kind === "offline")
164
+ return report(await fromCache(read.reason, read.guardRefusal, read.baseUrl));
165
+ if (!isProjectVersionSummary(read.summary)) {
166
+ const manifestUrl = read.summary.harness?.manifestUrl ?? null;
167
+ if (!manifestUrl)
168
+ throw new HarnessError(HARNESS_CODES.HARNESS_UNAVAILABLE, `Project ${projectId} resolves no harness version to mount.`);
169
+ const legacy = await mountLegacyManifest(root, manifestUrl, read.baseUrl);
170
+ return report({
171
+ projectId,
172
+ status: legacy.written.length > 0 ? "mounted" : "current",
173
+ source: "legacy",
174
+ version: legacy.version,
175
+ digest: legacy.digest,
176
+ runtime,
177
+ written: legacy.written,
178
+ kept: legacy.kept,
179
+ removed: [],
180
+ lines: [...lines, `harness mounted: ${legacy.version} (legacy) from ${manifestUrl}`],
181
+ });
134
182
  }
135
- await writeExcludeBlock(root, mounted);
136
- await mkdir(path.join(root, ".claude", "state"), { recursive: true });
137
- if (!(await readIfPresent(path.join(root, ".claude", "state", ".gitignore"))))
138
- await writeFile(path.join(root, ".claude", "state", ".gitignore"), "*\n");
139
- const state = { version, digest, mountedAt: new Date().toISOString(), manifestUrl };
140
- if (!prior || prior.digest !== digest || prior.version !== version || written > 0)
141
- await writeAtomic(path.join(root, MOUNT_STATE_PATH), `${JSON.stringify(state, null, 2)}\n`);
142
- return { version, digest, written, kept, via: "adapter" };
143
- }
144
- export async function mountHarness(root, manifestUrl, options = {}) {
145
- if (options.useEtna !== false) {
146
- const specifier = ETNA_MOUNT_SPECIFIER;
147
- const etna = (await import(specifier).catch(() => null));
148
- if (etna && typeof etna.mount === "function") {
149
- await etna.mount(root, manifestUrl);
150
- const state = await readMountState(root);
151
- return { version: state?.version ?? "", digest: state?.digest ?? "", written: 0, kept: 0, via: "etna" };
183
+ let resolved;
184
+ try {
185
+ resolved = await resolveProjectVersion(read.baseUrl, read.accessToken, projectId, pinned ? { digest: pinned.digest } : { track: "latest" });
186
+ }
187
+ catch (error) {
188
+ if (isOriginMismatch(error))
189
+ return report(await fromCache(error.message, error, read.baseUrl));
190
+ if (error instanceof VersionsRouteError && error.status === 404) {
191
+ if (error.code === "PROJECT_NOT_FOUND")
192
+ throw coded("PROJECT_NOT_FOUND", error.message);
193
+ if (error.code === "VALIDATION_FAILED" && pinned) {
194
+ const result = await mountVersion({ ...common, manifest: null, fetchObject: noFetch, expectedDigest: pinned.digest, recordExtras: { origin: read.baseUrl } });
195
+ if (result.status !== "mounted" && result.status !== "current") {
196
+ throw new HarnessError(HARNESS_CODES.HARNESS_DIGEST_MISMATCH, `Project ${projectId} serves no version with the pinned digest ${pinned.digest}, and none is cached.`);
197
+ }
198
+ return report(await handOver(root, runtime, outcomeOf(projectId, result, lines, HARNESS_CODES.HARNESS_DIGEST_MISMATCH)));
199
+ }
200
+ throw new HarnessError(HARNESS_CODES.HARNESS_UNAVAILABLE, `Project ${projectId} serves no harness version yet: ${error.message}`);
152
201
  }
202
+ if (error instanceof VersionsRouteError && (error.status === 0 || error.status >= 500))
203
+ return report(await fromCache(error.message, null, read.baseUrl));
204
+ throw new HarnessError(HARNESS_CODES.HARNESS_MOUNT_FAILED, error instanceof Error ? error.message : String(error));
205
+ }
206
+ let manifest;
207
+ try {
208
+ manifest = await fetchVersionManifest(read.baseUrl, read.accessToken, projectId, resolved.semver);
209
+ }
210
+ catch (error) {
211
+ if (error instanceof VersionsRouteError && (error.status === 0 || error.status >= 500))
212
+ return report(await fromCache(error.message, null, read.baseUrl));
213
+ throw new HarnessError(HARNESS_CODES.HARNESS_MOUNT_FAILED, error instanceof Error ? error.message : String(error));
153
214
  }
154
- return mountWithAdapter(root, manifestUrl, options);
215
+ const result = await mountVersion({
216
+ ...common,
217
+ manifest,
218
+ fetchObject: (entry) => fetchVersionObject(read.baseUrl, read.accessToken, projectId, resolved.semver, entry),
219
+ expectedDigest: pinned?.digest ?? resolved.digest,
220
+ recordExtras: { origin: read.baseUrl },
221
+ });
222
+ return report(await handOver(root, runtime, outcomeOf(projectId, result, lines, HARNESS_CODES.HARNESS_OFFLINE_NO_CACHE)));
155
223
  }
@@ -0,0 +1,43 @@
1
+ import { openLocalStoreReadOnly } from "../local_store/database.js";
2
+ const RECORDS_STORE_VERSION = 9;
3
+ const NEVER_EXPIRES = "9999-12-31";
4
+ const DISCHARGED_VERDICTS = new Set(["updated", "owes_nothing"]);
5
+ export async function readStoreDocumentationWaivers(projectId, dbPath) {
6
+ if (!projectId)
7
+ return [];
8
+ const store = await openLocalStoreReadOnly(dbPath ? { dbPath } : {}).catch(() => null);
9
+ if (!store)
10
+ return [];
11
+ try {
12
+ if (store.version < RECORDS_STORE_VERSION)
13
+ return [];
14
+ const rows = store.db
15
+ .prepare("SELECT payload_json FROM records WHERE kind = 'obligation' AND project_id = ?")
16
+ .all(projectId);
17
+ const subjects = new Set();
18
+ for (const row of rows) {
19
+ let payload;
20
+ try {
21
+ payload = JSON.parse(row.payload_json);
22
+ }
23
+ catch {
24
+ continue;
25
+ }
26
+ if (payload.type !== "documentation" || payload.state !== "discharged" || typeof payload.subject !== "string")
27
+ continue;
28
+ if (typeof payload.outcome?.verdict !== "string" || !DISCHARGED_VERDICTS.has(payload.outcome.verdict))
29
+ continue;
30
+ subjects.add(payload.subject);
31
+ }
32
+ return [...subjects].sort().map((subject) => ({
33
+ kind: "waived",
34
+ body: `bookkeeping \`${subject}\` — its documentation obligation is discharged in the local store expires=${NEVER_EXPIRES}`,
35
+ }));
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ finally {
41
+ store.close();
42
+ }
43
+ }
@@ -0,0 +1,29 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ const VENDORED = {
4
+ digest: "digest/index.js",
5
+ mount: "mount/index.js",
6
+ rules: "rules/index.js",
7
+ rulesNode: "rules/node.js",
8
+ };
9
+ const WORKSPACE = {
10
+ digest: "@nurix/harness/digest",
11
+ mount: "@nurix/harness/mount",
12
+ rules: "@nurix/harness/rules",
13
+ rulesNode: "@nurix/harness/rules/node",
14
+ };
15
+ const VENDOR_ROOT = new URL("../../vendor/harness/", import.meta.url);
16
+ export function isSourceRun(moduleUrl = import.meta.url) {
17
+ const file = fileURLToPath(moduleUrl);
18
+ return file.split(path.sep).join("/").includes("/packages/cli/src/");
19
+ }
20
+ const loaded = new Map();
21
+ export function loadHarness(name) {
22
+ let module = loaded.get(name);
23
+ if (!module) {
24
+ const specifier = isSourceRun() ? WORKSPACE[name] : new URL(VENDORED[name], VENDOR_ROOT).href;
25
+ module = import(specifier);
26
+ loaded.set(name, module);
27
+ }
28
+ return module;
29
+ }
@@ -0,0 +1,54 @@
1
+ import { authorizedFetch } from "../nustack_client.js";
2
+ import { assertSafeOpaqueId } from "../guards.js";
3
+ const REQUEST_TIMEOUT_MS = 15_000;
4
+ export class VersionsRouteError extends Error {
5
+ status;
6
+ code;
7
+ constructor(status, code, message) {
8
+ super(message);
9
+ this.name = "VersionsRouteError";
10
+ this.status = status;
11
+ this.code = code;
12
+ }
13
+ }
14
+ function versionsPath(projectId, ...segments) {
15
+ const safe = assertSafeOpaqueId("The Project id", projectId);
16
+ return `/api/v2/projects/${encodeURIComponent(safe)}/harness-versions${segments.map((segment) => `/${encodeURIComponent(segment)}`).join("")}`;
17
+ }
18
+ async function request(baseUrl, accessToken, routePath) {
19
+ try {
20
+ return await authorizedFetch(`${baseUrl}${routePath}`, {
21
+ headers: { Authorization: `Bearer ${accessToken}` },
22
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
23
+ });
24
+ }
25
+ catch (error) {
26
+ if (typeof error.code === "string" && error.code === "ORIGIN_MISMATCH")
27
+ throw error;
28
+ throw new VersionsRouteError(0, null, `${routePath} did not answer: ${error instanceof Error ? error.message : String(error)}`);
29
+ }
30
+ }
31
+ async function enveloped(response, label) {
32
+ const body = (await response.json().catch(() => null));
33
+ if (!response.ok || body?.data === undefined) {
34
+ const code = typeof body?.error?.code === "string" ? body.error.code : null;
35
+ const message = typeof body?.error?.message === "string" ? body.error.message : `HTTP ${response.status}`;
36
+ throw new VersionsRouteError(response.status, code, `${label}: ${message}`);
37
+ }
38
+ return body.data;
39
+ }
40
+ export async function resolveProjectVersion(baseUrl, accessToken, projectId, ask) {
41
+ const query = "digest" in ask ? `?digest=${encodeURIComponent(ask.digest)}` : "?track=latest";
42
+ const response = await request(baseUrl, accessToken, `${versionsPath(projectId, "resolve")}${query}`);
43
+ return enveloped(response, "harness version resolve failed");
44
+ }
45
+ export async function fetchVersionManifest(baseUrl, accessToken, projectId, semver) {
46
+ const response = await request(baseUrl, accessToken, versionsPath(projectId, semver, "manifest"));
47
+ return enveloped(response, `harness version ${semver} manifest read failed`);
48
+ }
49
+ export async function fetchVersionObject(baseUrl, accessToken, projectId, semver, entry) {
50
+ const response = await request(baseUrl, accessToken, versionsPath(projectId, semver, "files", ...entry.path.split("/")));
51
+ if (!response.ok)
52
+ await enveloped(response, `harness file ${entry.path} read failed`);
53
+ return new Uint8Array(await response.arrayBuffer());
54
+ }
@@ -54,6 +54,24 @@ export async function openLocalStore(options = {}) {
54
54
  },
55
55
  };
56
56
  }
57
+ export async function openLocalStoreReadOnly(options = {}) {
58
+ const dbFile = options.dbPath ?? stateDbPath();
59
+ if (!existsSync(dbFile))
60
+ return null;
61
+ const db = await withSuppressedSqliteWarning(async () => {
62
+ const { DatabaseSync } = await loadSqlite();
63
+ return new DatabaseSync(dbFile, { readOnly: true });
64
+ });
65
+ db.exec("PRAGMA busy_timeout = 2000");
66
+ const row = db.prepare("PRAGMA user_version").get();
67
+ return {
68
+ db,
69
+ version: Number(row?.user_version ?? 0),
70
+ close() {
71
+ db.close();
72
+ },
73
+ };
74
+ }
57
75
  export async function readStoreVersion(dbFile = stateDbPath()) {
58
76
  if (!existsSync(dbFile))
59
77
  return null;
@@ -135,3 +135,29 @@ export function gatePathOnce(store, sessionId, path, now = Date.now()) {
135
135
  store.db.prepare("DELETE FROM gated_paths WHERE gated_at < ?").run(now - GATED_PATH_RETENTION_MS);
136
136
  return isInserted;
137
137
  }
138
+ export const DIRECT_SESSION_ID = "direct";
139
+ export function recordBulkReadOutcome(store, transact, outcome, newId, now = Date.now()) {
140
+ const isDirect = outcome.sessionId === DIRECT_SESSION_ID;
141
+ transact(() => {
142
+ for (const entry of outcome.paths) {
143
+ insertLabelRow(store, {
144
+ id: newId(),
145
+ session_id: outcome.sessionId,
146
+ agent_type: outcome.agentType,
147
+ path: entry.realPath,
148
+ estimated_tokens: entry.estimatedTokens,
149
+ returned_tokens: entry.returnedTokens,
150
+ handler: "digest",
151
+ check_result: outcome.result,
152
+ bypass_used: 0,
153
+ latency_ms: outcome.latencyMs,
154
+ tokens_kept_out: -entry.returnedTokens,
155
+ created_at: now,
156
+ }, isDirect ? "local" : "pending");
157
+ if (outcome.result !== "pass")
158
+ gatePathOnce(store, outcome.sessionId, entry.realPath, now);
159
+ }
160
+ if (!isDirect && outcome.miss !== null)
161
+ recordRoutingMiss(store, outcome.sessionId, "read.bulk", outcome.miss, now);
162
+ });
163
+ }
@@ -2,10 +2,16 @@ import { assertSafeNuStackUrl, assertSafeOpaqueId } from "./guards.js";
2
2
  import { createSseParser } from "./sse_framing.js";
3
3
  import { deadCredentialCopy, DEVICE_HEADER, envToken, forgetRefusedToken, knownDeviceId, TokenRefusedError, userAgent } from "./auth_grant.js";
4
4
  import { getOrCreateDeviceId } from "./device_identity.js";
5
- const DEFAULT_NUSTACK_URL = "https://apps.nustack.ai";
5
+ import { readStoredCredential } from "./auth_grant.js";
6
+ import { readProjectContractSync } from "./project_contract.js";
7
+ import { contractOrigins, isContractOrigin, isPlatformDefault, originMismatchError, PLATFORM_DEFAULT_ORIGIN, recordContractOrigin, refusedContractOrigin, } from "./origin_guard.js";
8
+ const DEFAULT_NUSTACK_URL = PLATFORM_DEFAULT_ORIGIN;
9
+ export { contractOrigins };
6
10
  const REQUEST_TIMEOUT_MS = 10_000;
7
11
  export async function authorizedFetch(input, init = {}) {
8
12
  const headers = new Headers(init.headers);
13
+ if (headers.has("Authorization"))
14
+ await assertCredentialOrigin(String(input));
9
15
  headers.set(DEVICE_HEADER, knownDeviceId() ?? (await getOrCreateDeviceId()));
10
16
  headers.set("User-Agent", userAgent());
11
17
  const response = await globalThis.fetch(input, { ...init, headers });
@@ -33,8 +39,35 @@ export function errorMessage(payload, status, label, plane) {
33
39
  const enveloped = typeof payload?.error === "object" ? payload.error?.message : undefined;
34
40
  return enveloped ?? payload?.message ?? `${label}: HTTP ${status}`;
35
41
  }
42
+ export function resolveNuStackOrigin(options, cwd) {
43
+ if (options.nustackUrl)
44
+ return { url: assertSafeNuStackUrl(options.nustackUrl), source: "flag" };
45
+ if (process.env.NUSTACK_URL)
46
+ return { url: assertSafeNuStackUrl(process.env.NUSTACK_URL), source: "env" };
47
+ const origin = readProjectContractSync(cwd).origin;
48
+ if (origin) {
49
+ const url = assertSafeNuStackUrl(origin);
50
+ if (!isPlatformDefault(url)) {
51
+ recordContractOrigin(url);
52
+ if (process.env.NUSTACK_TOKEN?.trim())
53
+ throw originMismatchError(url, null, "pat");
54
+ }
55
+ return { url, source: "contract" };
56
+ }
57
+ return { url: assertSafeNuStackUrl(DEFAULT_NUSTACK_URL), source: "default" };
58
+ }
36
59
  export function resolveNuStackUrl(options) {
37
- return assertSafeNuStackUrl(options.nustackUrl ?? process.env.NUSTACK_URL ?? DEFAULT_NUSTACK_URL);
60
+ return resolveNuStackOrigin(options, process.cwd()).url;
61
+ }
62
+ export async function assertCredentialOrigin(url) {
63
+ if (contractOrigins.size === 0 || !isContractOrigin(url))
64
+ return;
65
+ if (process.env.NUSTACK_TOKEN?.trim())
66
+ throw originMismatchError(url, null, "pat");
67
+ const held = (await readStoredCredential().catch(() => null))?.credential ?? null;
68
+ const refused = refusedContractOrigin(held?.studioBaseUrl ?? null, url);
69
+ if (refused)
70
+ throw originMismatchError(refused, held?.studioBaseUrl ?? null, held ? "device" : "none");
38
71
  }
39
72
  export async function fetchCatalogue(baseUrl) {
40
73
  const response = await authorizedFetch(`${baseUrl}/api/catalog.json`, {
@@ -1,7 +1,7 @@
1
1
  import { readHeadCommit } from "../git-sync/core/repository.js";
2
2
  import { readSymbolicBranch } from "../git-sync/core/repository.js";
3
3
  import { reconcileInboundBinding } from "../git-sync/inbound/index.js";
4
- import { fetchToolkitProfile } from "../etna/profile.js";
4
+ import { fetchToolkitProfile, ToolkitProfileUnavailableError } from "../etna/profile.js";
5
5
  import { classifyFolder, attachRejection } from "./folder_state.js";
6
6
  import { runRepositoryOnboarding, } from "./binding.js";
7
7
  export async function attachExistingGit(ctx, options) {
@@ -24,7 +24,19 @@ export async function attachExistingGit(ctx, options) {
24
24
  if (!boundBranch) {
25
25
  return { ok: false, code: "DETACHED_HEAD", message: "HEAD is detached — check out a branch before attaching." };
26
26
  }
27
- const profile = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, options.projectId);
27
+ let profile;
28
+ try {
29
+ profile = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, options.projectId);
30
+ }
31
+ catch (error) {
32
+ if (error instanceof ToolkitProfileUnavailableError)
33
+ throw error;
34
+ return {
35
+ ok: false,
36
+ code: "PROJECT_SUMMARY_UNREACHABLE",
37
+ message: `The summary of Project ${options.projectId} could not be read, so this folder was not bound: ${error instanceof Error ? error.message : String(error)}`,
38
+ };
39
+ }
28
40
  const baselineCommitId = readHeadCommit(state.repository);
29
41
  const result = await runRepositoryOnboarding({
30
42
  store: ctx.store,