@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.
- package/dist/commands/agent.js +1 -53
- package/dist/commands/check.js +73 -0
- package/dist/commands/deploy.js +2 -1
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/harness.js +127 -48
- package/dist/commands/init.js +103 -3
- package/dist/commands/login.js +9 -2
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/project-sync.js +2 -1
- package/dist/commands/read.js +19 -0
- package/dist/commands/records.js +10 -0
- package/dist/commands/review.js +3 -2
- package/dist/commands/sessions.js +4 -1
- package/dist/commands/sync.js +2 -1
- package/dist/index.js +4 -0
- package/dist/lib/agent/claudeHooks.js +4 -1
- package/dist/lib/agent/desktopDoor.js +55 -0
- package/dist/lib/auth_grant.js +15 -3
- package/dist/lib/auth_recovery.js +10 -3
- package/dist/lib/etna/manager.js +24 -6
- package/dist/lib/etna/profile.js +6 -0
- package/dist/lib/etna/reconcile.js +6 -0
- package/dist/lib/gate/bulkReadRelay.js +354 -0
- package/dist/lib/gate/hook.js +30 -3
- package/dist/lib/gate/outline.js +12 -0
- package/dist/lib/gate/settings.js +24 -1
- package/dist/lib/gate/tierOne.js +22 -0
- package/dist/lib/harness/codes.js +24 -0
- package/dist/lib/harness/hookCheck.js +325 -0
- package/dist/lib/harness/hookEntry.js +27 -0
- package/dist/lib/harness/legacyMount.js +231 -0
- package/dist/lib/harness/mount.js +199 -131
- package/dist/lib/harness/storeWaivers.js +43 -0
- package/dist/lib/harness/vendor.js +29 -0
- package/dist/lib/harness/versionsClient.js +54 -0
- package/dist/lib/local_store/database.js +18 -0
- package/dist/lib/local_store/routing.js +26 -0
- package/dist/lib/nustack_client.js +35 -2
- package/dist/lib/onboarding/attach_existing_git.js +14 -2
- package/dist/lib/onboarding/binding.js +46 -2
- package/dist/lib/onboarding/commit_trailer_hook.js +66 -0
- package/dist/lib/onboarding/manifest_writer.js +19 -3
- package/dist/lib/onboarding/session_hook.js +76 -0
- package/dist/lib/origin_guard.js +43 -0
- package/dist/lib/project_contract.js +104 -0
- package/dist/lib/records/door.js +26 -19
- package/dist/lib/records/drain.js +10 -4
- package/dist/lib/records/refusalLog.js +30 -1
- package/dist/lib/session-engine/supervisor.js +6 -18
- package/dist/lib/sessions/client.js +0 -12
- package/dist/program.js +27 -4
- package/dist/vendor/harness/assemble/runtimes.js +32 -0
- package/dist/vendor/harness/assemble/settingsMerge.js +136 -0
- package/dist/vendor/harness/digest/index.js +38 -0
- package/dist/vendor/harness/lib/findings.js +32 -0
- package/dist/vendor/harness/lib/frontmatter.js +79 -0
- package/dist/vendor/harness/lib/paths.js +31 -0
- package/dist/vendor/harness/mount/cache.js +65 -0
- package/dist/vendor/harness/mount/exclude.js +77 -0
- package/dist/vendor/harness/mount/files.js +38 -0
- package/dist/vendor/harness/mount/index.js +352 -0
- package/dist/vendor/harness/rules/changeRules.js +162 -0
- package/dist/vendor/harness/rules/context.js +38 -0
- package/dist/vendor/harness/rules/docsRoot.js +34 -0
- package/dist/vendor/harness/rules/index.js +109 -0
- package/dist/vendor/harness/rules/node.js +144 -0
- package/dist/vendor/harness/rules/outputGate.js +28 -0
- package/dist/vendor/harness/rules/ownership.js +178 -0
- package/dist/vendor/harness/rules/packageTable.js +113 -0
- package/dist/vendor/harness/rules/plans.js +96 -0
- package/dist/vendor/harness/rules/sessionWalls.js +31 -0
- package/package.json +5 -5
|
@@ -1,155 +1,223 @@
|
|
|
1
|
-
import {
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
baseUrl = resolveNuStackOrigin(globals, root).url;
|
|
37
52
|
}
|
|
38
|
-
catch {
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|