@nurix/nustack 0.23.11 → 0.23.13

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 (63) hide show
  1. package/dist/commands/check.js +73 -0
  2. package/dist/commands/deploy.js +2 -1
  3. package/dist/commands/doctor.js +2 -1
  4. package/dist/commands/harness.js +127 -48
  5. package/dist/commands/init.js +103 -3
  6. package/dist/commands/login.js +9 -2
  7. package/dist/commands/mcp.js +7 -0
  8. package/dist/commands/project-sync.js +2 -1
  9. package/dist/commands/records.js +10 -0
  10. package/dist/commands/review.js +3 -2
  11. package/dist/commands/sessions.js +4 -1
  12. package/dist/commands/sync.js +2 -1
  13. package/dist/index.js +4 -0
  14. package/dist/lib/agent/claudeHooks.js +4 -1
  15. package/dist/lib/auth_grant.js +15 -3
  16. package/dist/lib/auth_recovery.js +10 -3
  17. package/dist/lib/etna/manager.js +24 -6
  18. package/dist/lib/etna/profile.js +6 -0
  19. package/dist/lib/etna/reconcile.js +6 -0
  20. package/dist/lib/harness/codes.js +24 -0
  21. package/dist/lib/harness/hookCheck.js +325 -0
  22. package/dist/lib/harness/hookEntry.js +27 -0
  23. package/dist/lib/harness/legacyMount.js +231 -0
  24. package/dist/lib/harness/mount.js +199 -131
  25. package/dist/lib/harness/storeWaivers.js +43 -0
  26. package/dist/lib/harness/vendor.js +29 -0
  27. package/dist/lib/harness/versionsClient.js +54 -0
  28. package/dist/lib/local_store/database.js +18 -0
  29. package/dist/lib/nustack_client.js +35 -2
  30. package/dist/lib/onboarding/attach_existing_git.js +14 -2
  31. package/dist/lib/onboarding/binding.js +46 -2
  32. package/dist/lib/onboarding/commit_trailer_hook.js +66 -0
  33. package/dist/lib/onboarding/manifest_writer.js +19 -3
  34. package/dist/lib/onboarding/session_hook.js +76 -0
  35. package/dist/lib/origin_guard.js +43 -0
  36. package/dist/lib/project_contract.js +104 -0
  37. package/dist/lib/records/door.js +26 -19
  38. package/dist/lib/records/drain.js +10 -4
  39. package/dist/lib/records/refusalLog.js +30 -1
  40. package/dist/lib/session-engine/supervisor.js +6 -18
  41. package/dist/lib/sessions/client.js +0 -12
  42. package/dist/program.js +14 -3
  43. package/dist/vendor/harness/assemble/runtimes.js +32 -0
  44. package/dist/vendor/harness/assemble/settingsMerge.js +136 -0
  45. package/dist/vendor/harness/digest/index.js +38 -0
  46. package/dist/vendor/harness/lib/findings.js +32 -0
  47. package/dist/vendor/harness/lib/frontmatter.js +79 -0
  48. package/dist/vendor/harness/lib/paths.js +31 -0
  49. package/dist/vendor/harness/mount/cache.js +65 -0
  50. package/dist/vendor/harness/mount/exclude.js +77 -0
  51. package/dist/vendor/harness/mount/files.js +38 -0
  52. package/dist/vendor/harness/mount/index.js +352 -0
  53. package/dist/vendor/harness/rules/changeRules.js +162 -0
  54. package/dist/vendor/harness/rules/context.js +38 -0
  55. package/dist/vendor/harness/rules/docsRoot.js +34 -0
  56. package/dist/vendor/harness/rules/index.js +109 -0
  57. package/dist/vendor/harness/rules/node.js +144 -0
  58. package/dist/vendor/harness/rules/outputGate.js +28 -0
  59. package/dist/vendor/harness/rules/ownership.js +178 -0
  60. package/dist/vendor/harness/rules/packageTable.js +113 -0
  61. package/dist/vendor/harness/rules/plans.js +96 -0
  62. package/dist/vendor/harness/rules/sessionWalls.js +31 -0
  63. package/package.json +5 -5
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ import { resolveRepository } from "../lib/git-sync/core/repository.js";
3
+ import { loadHarness } from "../lib/harness/vendor.js";
4
+ import { readStoreDocumentationWaivers } from "../lib/harness/storeWaivers.js";
5
+ import { runHookMode } from "../lib/harness/hookEntry.js";
6
+ import { readProjectContract } from "../lib/project_contract.js";
7
+ import { NUSTACK_JSON_VERSION } from "../lib/json_output.js";
8
+ export const CHECK_EXIT = { PASS: 0, FAILED: 1, CANNOT_RUN: 2 };
9
+ const COMMAND = "check";
10
+ function cannotRun(json, rule, code, message) {
11
+ if (json)
12
+ console.log(JSON.stringify({ nustackJsonVersion: NUSTACK_JSON_VERSION, command: COMMAND, ok: false, error: { code, message }, data: { rule, verdict: null, findings: [] } }));
13
+ else
14
+ console.error(message);
15
+ process.exitCode = CHECK_EXIT.CANNOT_RUN;
16
+ }
17
+ function rootOf(dir) {
18
+ try {
19
+ return resolveRepository(dir).repositoryRoot;
20
+ }
21
+ catch {
22
+ return dir;
23
+ }
24
+ }
25
+ export async function runCheck(rule, options) {
26
+ if (options.hook !== undefined) {
27
+ await runHookMode(options.hook, options.protection === true);
28
+ return;
29
+ }
30
+ const json = options.json === true;
31
+ if (!rule) {
32
+ cannotRun(json, null, "VALIDATION_FAILED", "Name a rule: `nustack check <rule>`, or pass `--hook <event>`.");
33
+ return;
34
+ }
35
+ let rules;
36
+ let rulesNode;
37
+ try {
38
+ rules = await loadHarness("rules");
39
+ rulesNode = await loadHarness("rulesNode");
40
+ }
41
+ catch (error) {
42
+ cannotRun(json, rule, "HARNESS_UNAVAILABLE", `The rule library did not load: ${error instanceof Error ? error.message : String(error)}`);
43
+ return;
44
+ }
45
+ if (!rules.isRuleId(rule)) {
46
+ cannotRun(json, rule, "UNKNOWN_RULE", `Unknown rule \`${rule}\`; this CLI serves ${rules.RULE_IDS.join(", ")}.`);
47
+ return;
48
+ }
49
+ const root = rootOf(path.resolve(process.cwd()));
50
+ let result;
51
+ try {
52
+ const waivers = rulesNode.readBookkeepingWaivers(root);
53
+ if (rule === "bookkeeping") {
54
+ const contract = await readProjectContract(root);
55
+ waivers.push(...(await readStoreDocumentationWaivers(contract.projectId)));
56
+ }
57
+ result = rules.runRule(rule, { ...rulesNode.nodeRuleContext({ root }), waivers });
58
+ }
59
+ catch (error) {
60
+ cannotRun(json, rule, "RULE_CANNOT_RUN", `${rule} could not run: ${error instanceof Error ? error.message : String(error)}`);
61
+ return;
62
+ }
63
+ const exit = result.verdict === "pass" ? CHECK_EXIT.PASS : CHECK_EXIT.FAILED;
64
+ if (json) {
65
+ console.log(JSON.stringify({ nustackJsonVersion: NUSTACK_JSON_VERSION, command: COMMAND, ok: true, data: { rule, verdict: result.verdict, findings: result.findings } }));
66
+ }
67
+ else {
68
+ console.log(`${rule}: ${result.verdict}`);
69
+ for (const finding of result.findings)
70
+ console.log(` ${finding.path ? `${finding.path}: ` : ""}${finding.message}`);
71
+ }
72
+ process.exitCode = exit;
73
+ }
@@ -6,6 +6,7 @@ import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
6
6
  import { resolveNuStackUrl } from "../lib/nustack_client.js";
7
7
  import { resolveProjectId } from "../lib/project_context.js";
8
8
  import { projectOverviewJson, readProjectOverview } from "./project.js";
9
+ import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
9
10
  const DEFAULT_TIMEOUT_MINUTES = 15;
10
11
  export function emitFail(command, json, code, message) {
11
12
  if (json)
@@ -173,7 +174,7 @@ export async function runStatus(options, globals) {
173
174
  return;
174
175
  }
175
176
  const baseUrl = resolveNuStackUrl(globals);
176
- const auth = await ensureAccessToken().catch(() => null);
177
+ const auth = await ensureAccessToken().catch(nullUnlessOriginRefused);
177
178
  const overview = await readProjectOverview(project.projectId, cwd, baseUrl, auth?.accessToken ?? null);
178
179
  let hosting = null;
179
180
  if (auth && overview.cloudProject) {
@@ -21,6 +21,7 @@ import { NotARepositoryError } from "../lib/git-sync/core/git_runner.js";
21
21
  import { listBundles, sweepExpiredBundles } from "../lib/git-sync/recovery/store.js";
22
22
  import { inspectRecoveryKey } from "../lib/git-sync/recovery/crypto.js";
23
23
  import { NUSTACK_JSON_VERSION } from "../lib/json_output.js";
24
+ import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
24
25
  async function credentialStoreRows(environmentInUse) {
25
26
  if (environmentInUse)
26
27
  return [{ check: "Credential source", status: "ok", detail: "environment — no store is read" }];
@@ -58,7 +59,7 @@ export async function runDoctor(options) {
58
59
  rows.push({ area, check, status, detail });
59
60
  };
60
61
  const fromEnv = envToken();
61
- const deviceAuth = await ensureAccessToken().catch(() => null);
62
+ const deviceAuth = await ensureAccessToken().catch(nullUnlessOriginRefused);
62
63
  if (fromEnv?.kind === "malformed")
63
64
  add("identity", "Device sign-in", "error", fromEnv.message);
64
65
  else if (fromEnv)
@@ -1,10 +1,14 @@
1
1
  import path from "node:path";
2
- import { getProject, resolveNuStackUrl } from "../lib/nustack_client.js";
3
- import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
4
- import { resolveProjectId } from "../lib/project_context.js";
5
2
  import { resolveRepository } from "../lib/git-sync/core/repository.js";
6
- import { mountHarness, readMountState } from "../lib/harness/mount.js";
3
+ import { isProjectVersionSummary, mountProjectHarness, projectOf, readMountState, readSummary } from "../lib/harness/mount.js";
4
+ import { readLegacyMountRecord } from "../lib/harness/legacyMount.js";
5
+ import { resolveProjectVersion } from "../lib/harness/versionsClient.js";
6
+ import { HARNESS_CODES, STATUS_EXIT } from "../lib/harness/codes.js";
7
+ import { installCommitTrailerHook, trailerHookLine } from "../lib/onboarding/commit_trailer_hook.js";
8
+ import { readProjectContract } from "../lib/project_contract.js";
7
9
  import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
10
+ const RUNTIMES = ["claude-code", "codex", "opencode", "copilot-cli"];
11
+ const WRITE_CONTRACT_MODES = ["never", "when-unmodified"];
8
12
  function emitFail(command, json, code, message) {
9
13
  if (json)
10
14
  printJsonError(command, { code, message });
@@ -13,6 +17,10 @@ function emitFail(command, json, code, message) {
13
17
  process.exitCode = 1;
14
18
  }
15
19
  }
20
+ function codeOf(error) {
21
+ const code = error?.code;
22
+ return typeof code === "string" ? code : HARNESS_CODES.HARNESS_MOUNT_FAILED;
23
+ }
16
24
  function rootOf(dir) {
17
25
  try {
18
26
  return resolveRepository(dir).repositoryRoot;
@@ -21,70 +29,141 @@ function rootOf(dir) {
21
29
  return dir;
22
30
  }
23
31
  }
24
- async function resolveProjectHarness(command, json, root, options, globals) {
25
- const project = await resolveProjectId(root, options);
26
- if (!project.ok) {
27
- emitFail(command, json, "PROJECT_CONTEXT_MISSING", project.message);
28
- return null;
29
- }
30
- const baseUrl = resolveNuStackUrl(globals);
31
- const auth = await ensureAuthWithRecovery(baseUrl, json);
32
- if (!auth) {
33
- const failure = lastAuthFailure();
34
- emitFail(command, json, failure.code, failure.message);
35
- return null;
36
- }
37
- const summary = await getProject(baseUrl, auth.accessToken, project.projectId);
38
- return { projectId: project.projectId, harness: summary.harness ?? null };
32
+ function contractView(contract) {
33
+ return {
34
+ projectId: contract.projectId,
35
+ harness: contract.harness,
36
+ origin: contract.origin,
37
+ door: contract.door ?? null,
38
+ docsRoot: contract.docsRoot,
39
+ branchPolicy: contract.branchPolicy,
40
+ };
39
41
  }
40
42
  export async function runHarnessMount(target, options, globals) {
41
43
  const json = options.json === true;
42
44
  const root = rootOf(path.resolve(target ?? process.cwd()));
43
- let manifestUrl = options.manifest ?? null;
44
- let projectId = null;
45
- if (!manifestUrl) {
46
- const cloud = await resolveProjectHarness("harness mount", json, root, options, globals);
47
- if (!cloud)
48
- return;
49
- projectId = cloud.projectId;
50
- manifestUrl = cloud.harness?.manifestUrl ?? null;
51
- if (!manifestUrl) {
52
- emitFail("harness mount", json, "HARNESS_UNAVAILABLE", `Project ${cloud.projectId} resolves no harness version with an uploaded manifest.`);
53
- return;
54
- }
45
+ const runtime = options.runtime ?? "claude-code";
46
+ if (!RUNTIMES.includes(runtime)) {
47
+ emitFail("harness mount", json, "VALIDATION_FAILED", `--runtime must be one of ${RUNTIMES.join(", ")}.`);
48
+ return;
55
49
  }
56
- let mounted;
50
+ const writeContract = options.writeContract ?? "never";
51
+ if (!WRITE_CONTRACT_MODES.includes(writeContract)) {
52
+ emitFail("harness mount", json, "VALIDATION_FAILED", `--write-contract must be one of ${WRITE_CONTRACT_MODES.join(", ")}.`);
53
+ return;
54
+ }
55
+ let outcome;
57
56
  try {
58
- mounted = await mountHarness(root, manifestUrl);
57
+ outcome = await mountProjectHarness(root, {
58
+ globals,
59
+ projectId: options.project,
60
+ runtime: runtime,
61
+ writeContract: writeContract,
62
+ log: (line) => console.error(line),
63
+ });
59
64
  }
60
65
  catch (error) {
61
- emitFail("harness mount", json, "HARNESS_MOUNT_FAILED", `The harness mount failed: ${error instanceof Error ? error.message : String(error)}`);
66
+ emitFail("harness mount", json, codeOf(error), error instanceof Error ? error.message : String(error));
62
67
  return;
63
68
  }
69
+ const trailerHook = await installCommitTrailerHook(root);
70
+ const trailerLine = trailerHookLine(trailerHook);
71
+ if (trailerLine && trailerHook !== "skipped_not_git")
72
+ console.error(trailerLine);
64
73
  if (json) {
65
- printJsonSuccess("harness mount", { projectId, root, manifestUrl, ...mounted });
74
+ const { lines: _lines, ...mounted } = outcome;
75
+ printJsonSuccess("harness mount", { ...mounted, root, trailerHook });
66
76
  return;
67
77
  }
68
- console.log(`Mounted harness ${mounted.version} into ${root} (${mounted.written} written, ${mounted.kept} unchanged, digest ${mounted.digest.slice(0, 12)})`);
78
+ console.log(`Mounted harness ${outcome.version ?? "(unknown)"} for ${outcome.runtime} into ${root} from ${outcome.source} ` +
79
+ `(${outcome.written.length} written, ${outcome.kept.length} unchanged, ${outcome.removed.length} removed${outcome.digest ? `, digest ${outcome.digest.slice(0, 12)}` : ""})`);
69
80
  }
70
81
  export async function runHarnessStatus(options, globals) {
71
82
  const json = options.json === true;
72
83
  const root = rootOf(process.cwd());
73
- const cloud = await resolveProjectHarness("harness status", json, root, options, globals);
74
- if (!cloud)
84
+ const contract = await readProjectContract(root);
85
+ let projectId;
86
+ try {
87
+ projectId = await projectOf(root, options.project, contract.projectId);
88
+ }
89
+ catch (error) {
90
+ emitFail("harness status", json, codeOf(error), error instanceof Error ? error.message : String(error));
91
+ return;
92
+ }
93
+ const pinned = contract.harness && "digest" in contract.harness ? contract.harness : null;
94
+ let reading;
95
+ try {
96
+ const read = await readSummary(root, projectId, globals);
97
+ if (read.kind === "offline") {
98
+ if (read.guardRefusal) {
99
+ emitFail("harness status", json, read.guardRefusal.code, read.guardRefusal.message);
100
+ return;
101
+ }
102
+ reading = pinned
103
+ ? { expected: { version: pinned.pin, digest: pinned.digest }, source: "version", unreachable: null }
104
+ : { expected: null, source: "version", unreachable: read.reason };
105
+ }
106
+ else if (!isProjectVersionSummary(read.summary)) {
107
+ const version = read.summary.harness?.version ?? null;
108
+ if (version === null) {
109
+ emitFail("harness status", json, HARNESS_CODES.HARNESS_UNAVAILABLE, `Project ${projectId} resolves no harness version.`);
110
+ return;
111
+ }
112
+ reading = { expected: { version, digest: null }, source: "legacy", unreachable: null };
113
+ }
114
+ else if (pinned) {
115
+ reading = { expected: { version: pinned.pin, digest: pinned.digest }, source: "version", unreachable: null };
116
+ }
117
+ else {
118
+ try {
119
+ const resolved = await resolveProjectVersion(read.baseUrl, read.accessToken, projectId, { track: "latest" });
120
+ reading = { expected: { version: resolved.semver, digest: resolved.digest }, source: "version", unreachable: null };
121
+ }
122
+ catch (error) {
123
+ if (codeOf(error) === HARNESS_CODES.ORIGIN_MISMATCH) {
124
+ emitFail("harness status", json, HARNESS_CODES.ORIGIN_MISMATCH, error instanceof Error ? error.message : String(error));
125
+ return;
126
+ }
127
+ reading = { expected: null, source: "version", unreachable: error instanceof Error ? error.message : String(error) };
128
+ }
129
+ }
130
+ }
131
+ catch (error) {
132
+ emitFail("harness status", json, codeOf(error), error instanceof Error ? error.message : String(error));
75
133
  return;
76
- const mounted = await readMountState(root);
77
- const pinned = cloud.harness?.version ?? null;
78
- const inStep = pinned === null || mounted?.version === pinned;
134
+ }
135
+ let mounted;
136
+ if (reading.source === "legacy") {
137
+ const legacy = await readLegacyMountRecord(root);
138
+ mounted = legacy ? { version: legacy.version, digest: legacy.digest, mountedAt: legacy.mountedAt, source: "legacy", origin: legacy.origin } : null;
139
+ }
140
+ else {
141
+ mounted = await readMountState(root);
142
+ if (mounted?.source === "legacy")
143
+ mounted = null;
144
+ }
145
+ const inStep = reading.expected === null
146
+ ? null
147
+ : reading.source === "legacy"
148
+ ? mounted !== null && mounted.version === reading.expected.version
149
+ : mounted !== null && mounted.digest === reading.expected.digest;
150
+ const exit = inStep === null ? STATUS_EXIT.UNREACHABLE : inStep ? STATUS_EXIT.IN_STEP : STATUS_EXIT.DIFFERING;
79
151
  if (json) {
80
- printJsonSuccess("harness status", { projectId: cloud.projectId, mounted, cloud: cloud.harness, inStep });
152
+ printJsonSuccess("harness status", {
153
+ projectId,
154
+ contract: contractView(contract),
155
+ mounted,
156
+ expected: reading.expected,
157
+ inStep,
158
+ source: reading.source,
159
+ ...(reading.unreachable ? { unreachable: reading.unreachable } : {}),
160
+ });
81
161
  }
82
162
  else {
83
- console.log(`Harness for Project ${cloud.projectId}`);
84
- console.log(` mounted: ${mounted ? `${mounted.version} (digest ${mounted.digest.slice(0, 12)})` : "(nothing mounted)"}`);
85
- console.log(` cloud pin: ${cloud.harness ? `${cloud.harness.slug} ${cloud.harness.version}` : "(none)"}`);
86
- console.log(` verdict: ${inStep ? "in step" : "DIFFERS — run `nustack harness mount`"}`);
163
+ console.log(`Harness for Project ${projectId} (${reading.source === "legacy" ? "legacy harness" : "project version"})`);
164
+ console.log(` mounted: ${mounted ? `${mounted.version} (digest ${mounted.digest.slice(0, 12)}, ${mounted.source ?? "unknown source"})` : "(nothing mounted)"}`);
165
+ console.log(` expected: ${reading.expected ? `${reading.expected.version ?? "?"}${reading.expected.digest ? ` (digest ${reading.expected.digest.slice(0, 12)})` : ""}` : `(unreachable: ${reading.unreachable ?? "unknown"})`}`);
166
+ console.log(` verdict: ${inStep === null ? "UNKNOWN — the expected version could not be read" : inStep ? "in step" : "DIFFERS — run `nustack harness mount`"}`);
87
167
  }
88
- if (!inStep)
89
- process.exitCode = 1;
168
+ process.exitCode = exit;
90
169
  }
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import * as p from "@clack/prompts";
4
4
  import { createProjectViaAsk, createOrganization, getProject, listProjects, listOrganizations, resolveNuStackUrl, } from "../lib/nustack_client.js";
5
- import { ensureAccessToken } from "../lib/auth_grant.js";
5
+ import { ensureAccessToken, envToken } from "../lib/auth_grant.js";
6
6
  import { getOrCreateDeviceId } from "../lib/device_identity.js";
7
7
  import { isInteractive } from "../lib/interactive.js";
8
8
  import { resolveOrganizationId } from "../lib/project_context.js";
@@ -15,9 +15,28 @@ import { attachExistingGit } from "../lib/onboarding/attach_existing_git.js";
15
15
  import { initFreshFolder } from "../lib/onboarding/init_fresh_folder.js";
16
16
  import { webFirstAttach } from "../lib/onboarding/web_first_attach.js";
17
17
  import { performDeviceLogin } from "./login.js";
18
+ import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
19
+ import { readProjectContract } from "../lib/project_contract.js";
20
+ import { HARNESS_CODES } from "../lib/harness/codes.js";
21
+ import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
18
22
  export async function runInit(options) {
19
23
  const cwd = process.cwd();
20
24
  const baseUrl = resolveNuStackUrl(options);
25
+ const isUnattended = options.yes === true && Boolean(options.project?.trim()) && Boolean(options.organization?.trim());
26
+ if (isUnattended) {
27
+ await runUnattendedInit(baseUrl, options);
28
+ return;
29
+ }
30
+ if (options.json || !isInteractive()) {
31
+ const message = "nustack init with no terminal needs `--yes --project <id> --organization <id>` (it never prompts, opens a browser or creates a Project); a credential comes from NUSTACK_TOKEN or `nustack login`.";
32
+ if (options.json)
33
+ printJsonError("init", { code: HARNESS_CODES.INIT_NEEDS_PROJECT, message });
34
+ else {
35
+ console.error(message);
36
+ process.exitCode = 1;
37
+ }
38
+ return;
39
+ }
21
40
  p.intro("nustack init");
22
41
  if (!isInteractive()) {
23
42
  p.log.error("nustack init needs an interactive terminal (browser sign-in + prompts).");
@@ -34,8 +53,87 @@ export async function runInit(options) {
34
53
  organization: options.organization,
35
54
  });
36
55
  }
56
+ function initEnvelope(result) {
57
+ return {
58
+ projectId: result.projectId,
59
+ organizationId: result.organizationId,
60
+ contract: result.projectManifest,
61
+ sessionHook: result.sessionHook,
62
+ mount: result.mount,
63
+ trailerHook: result.trailerHook,
64
+ };
65
+ }
66
+ async function runUnattendedInit(baseUrl, options) {
67
+ const json = options.json === true;
68
+ const projectId = options.project.trim();
69
+ const organizationId = options.organization.trim();
70
+ const fail = (code, message) => {
71
+ if (json)
72
+ printJsonError("init", { code, message });
73
+ else {
74
+ console.error(message);
75
+ process.exitCode = 1;
76
+ }
77
+ };
78
+ if (envToken()?.kind === "ok") {
79
+ fail(HARNESS_CODES.INIT_NEEDS_DEVICE, "An unattended init binds this folder to the Project as this machine, which takes a device sign-in: run `nustack login` (or `nustack login --manual`) once, and unset NUSTACK_TOKEN for the init.");
80
+ return;
81
+ }
82
+ const auth = await ensureAccessToken(baseUrl).catch(nullUnlessOriginRefused);
83
+ if (!auth) {
84
+ fail("SIGNED_OUT", "This machine is not signed in: run `nustack login` (or `nustack login --manual`) once first. An unattended init never opens a browser, and a NUSTACK_TOKEN cannot bind a folder.");
85
+ return;
86
+ }
87
+ const cwd = process.cwd();
88
+ const bound = (await readProjectContract(cwd)).projectId;
89
+ if (bound && bound !== projectId) {
90
+ fail("REBIND_CONFIRMATION_REQUIRED", `This folder is bound to Project ${bound}. An unattended init never rebinds it to ${projectId}; run \`nustack init\` in a terminal to confirm a rebind.`);
91
+ return;
92
+ }
93
+ const store = await openLocalStore();
94
+ try {
95
+ const device = readDeviceState(store);
96
+ const ctx = {
97
+ store,
98
+ baseUrl,
99
+ accessToken: auth.accessToken,
100
+ deviceRef: device?.deviceRef ?? (await getOrCreateDeviceId()),
101
+ userId: device?.activeUserId ?? null,
102
+ cwd,
103
+ json,
104
+ yes: true,
105
+ interactive: false,
106
+ };
107
+ const state = classifyFolder(cwd);
108
+ let outcome;
109
+ if (state.class === "git_worktree" && state.repository && state.git) {
110
+ const root = state.repository.repositoryRoot;
111
+ if (!hasManifest(root))
112
+ await scaffoldManifest(root, deriveRepoName(path.basename(root)));
113
+ outcome = await attachExistingGit(ctx, { projectId, organizationId, confirmRebind: false });
114
+ }
115
+ else {
116
+ outcome = await webFirstAttach(ctx, { projectId, organizationId, confirmPopulated: true });
117
+ }
118
+ if (!outcome.ok) {
119
+ fail(outcome.code, outcome.message);
120
+ return;
121
+ }
122
+ if (json) {
123
+ printJsonSuccess("init", initEnvelope(outcome.result));
124
+ return;
125
+ }
126
+ for (const notice of outcome.result.notices)
127
+ console.error(notice);
128
+ console.log(`Bound to Project ${outcome.result.projectId} on branch ${outcome.result.boundBranch} (binding ${outcome.result.bindingId}).`);
129
+ console.log(`Harness: ${outcome.result.mount.status}${outcome.result.mount.version ? ` ${outcome.result.mount.version}` : ""} · session hook ${outcome.result.sessionHook} · trailer hook ${outcome.result.trailerHook}.`);
130
+ }
131
+ finally {
132
+ store.close();
133
+ }
134
+ }
37
135
  export async function ensureSignedIn(baseUrl, cwd) {
38
- const deviceAuth = await ensureAccessToken().catch(() => null);
136
+ const deviceAuth = await ensureAccessToken().catch(nullUnlessOriginRefused);
39
137
  if (deviceAuth) {
40
138
  p.log.success(`Signed in as ${deviceAuth.userEmail}`);
41
139
  return deviceAuth.accessToken;
@@ -45,7 +143,7 @@ export async function ensureSignedIn(baseUrl, cwd) {
45
143
  process.exitCode = 1;
46
144
  return null;
47
145
  }
48
- const refreshed = await ensureAccessToken().catch(() => null);
146
+ const refreshed = await ensureAccessToken().catch(nullUnlessOriginRefused);
49
147
  if (!refreshed) {
50
148
  p.outro("Sign-in did not complete — run `nustack login` and retry.");
51
149
  process.exitCode = 1;
@@ -253,6 +351,8 @@ async function confirm(message) {
253
351
  return !p.isCancel(answer) && answer === true;
254
352
  }
255
353
  function reportSuccess(result) {
354
+ for (const notice of result.notices)
355
+ p.log.warn(notice);
256
356
  p.log.success(`Bound to Project ${result.projectId} on branch ${result.boundBranch} (binding ${result.bindingId}).`);
257
357
  p.log.info(`Toolkit ${result.toolkit.name}@${result.toolkit.version}. No Git push, pull, or remote was created.`);
258
358
  p.outro("Ready — `nustack status` · `nustack docs list` · `nustack project sync now`.");
@@ -3,7 +3,8 @@ import { resolveNuStackUrl } from "../lib/nustack_client.js";
3
3
  import { launchBrowser } from "../lib/browser_opener.js";
4
4
  import { selectCredentialStore } from "../lib/credential_store.js";
5
5
  import { isInteractive } from "../lib/interactive.js";
6
- import { ensureAccessToken, ENV_TOKEN_VARIABLE, envToken, loginWithLoopback, loginWithPastedCode, prepareLogin, } from "../lib/auth_grant.js";
6
+ import { ensureAccessToken, ENV_TOKEN_VARIABLE, envToken, loginWithLoopback, loginWithPastedCode, prepareLogin, readStoredCredential, } from "../lib/auth_grant.js";
7
+ import { isContractOrigin, nullUnlessOriginRefused, originMismatchError, refusedContractOrigin } from "../lib/origin_guard.js";
7
8
  async function readStdinLine() {
8
9
  return new Promise((resolve) => {
9
10
  let buffered = "";
@@ -77,6 +78,12 @@ async function loopbackFlow(ctx, flags) {
77
78
  }
78
79
  }
79
80
  export async function performDeviceLogin(baseUrl, _cwd = process.cwd(), flags = {}) {
81
+ if (isContractOrigin(baseUrl)) {
82
+ const held = envToken() ? null : (await readStoredCredential().catch(() => null))?.credential ?? null;
83
+ const refused = refusedContractOrigin(held?.studioBaseUrl ?? null, baseUrl);
84
+ if (refused)
85
+ throw originMismatchError(refused, held?.studioBaseUrl ?? null, envToken() ? "pat" : held ? "device" : "none");
86
+ }
80
87
  if (envToken()) {
81
88
  p.log.info(`${ENV_TOKEN_VARIABLE} is set — this process authenticates with it, and no sign-in is stored on this machine. Unset it to sign in as a person.`);
82
89
  return true;
@@ -90,7 +97,7 @@ export async function performDeviceLogin(baseUrl, _cwd = process.cwd(), flags =
90
97
  return false;
91
98
  }
92
99
  if (!flags.force && !flags.reauthorize) {
93
- const live = await ensureAccessToken().catch(() => null);
100
+ const live = await ensureAccessToken().catch(nullUnlessOriginRefused);
94
101
  if (live) {
95
102
  p.log.success(`Signed in as ${live.userEmail || "this account"} — already signed in on this machine (use --force to sign in again).`);
96
103
  return true;
@@ -1,3 +1,4 @@
1
+ import { resolveAccessToken } from "../lib/auth_grant.js";
1
2
  import { ensureAuthWithRecovery } from "../lib/auth_recovery.js";
2
3
  import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
3
4
  import { authorizedFetch, resolveNuStackUrl } from "../lib/nustack_client.js";
@@ -10,6 +11,12 @@ async function runServe(options, globals) {
10
11
  const bound = flag ? null : await resolveProjectId(process.cwd(), {});
11
12
  const defaultProjectId = flag ?? (bound?.ok ? bound.projectId : null);
12
13
  const doorUrl = doorUrlFor(baseUrl, process.env.NUSTACK_MCP_URL);
14
+ const outcome = await resolveAccessToken(baseUrl).catch(() => null);
15
+ if (outcome?.kind === "origin_mismatch") {
16
+ process.stderr.write(`nustack mcp: ${outcome.error.message}\n`);
17
+ process.exitCode = 1;
18
+ return;
19
+ }
13
20
  process.stderr.write(`nustack mcp: relaying to ${doorUrl}${defaultProjectId ? `, default project ${defaultProjectId}` : ""}\n`);
14
21
  await runRelay({
15
22
  doorUrl,
@@ -12,6 +12,7 @@ import { listBundles, restoreBundle, deleteBundle, sweepExpiredBundles, } from "
12
12
  import { resolveRepository } from "../lib/git-sync/core/repository.js";
13
13
  import { registerOutbound, createUploadClient, deviceTokenProvider, describeHistoryDecision, publishCurrentState, adoptWithoutPublishing, } from "../lib/git-sync/outbound/index.js";
14
14
  import { latestAcceptedUpload } from "../lib/local_store/accepted_uploads.js";
15
+ import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
15
16
  const ONBOARDING_POINTER = "No NuStack Project bindings on this machine. Run `nustack project attach` inside a Git repository to bind it.";
16
17
  function writeLine(line) {
17
18
  process.stdout.write(`${line}\n`);
@@ -92,7 +93,7 @@ export function registerProjectSyncCommands(projectCommand, getGlobalOptions) {
92
93
  }
93
94
  async function buildInboundRuntime(store, globals) {
94
95
  const baseUrl = resolveNuStackUrl(globals);
95
- const auth = await ensureAccessToken().catch(() => null);
96
+ const auth = await ensureAccessToken().catch(nullUnlessOriginRefused);
96
97
  if (!auth)
97
98
  return null;
98
99
  const deviceState = readDeviceState(store);
@@ -3,6 +3,7 @@ import { readDesktopSettings } from "../lib/desktop_settings.js";
3
3
  import { openLocalStore } from "../lib/local_store/database.js";
4
4
  import { stateDbPath } from "../lib/local_store/paths.js";
5
5
  import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
6
+ import { HARNESS_CODES } from "../lib/harness/codes.js";
6
7
  import { resolveProjectId } from "../lib/project_context.js";
7
8
  import { isEmitKind } from "../lib/records/door.js";
8
9
  import { acquireDrainLock, drainRecords, refreshDrainLock, releaseDrainLock, startDetachedDrain } from "../lib/records/drain.js";
@@ -177,6 +178,15 @@ export async function runRecordsSync(options, globals) {
177
178
  if (project.ok)
178
179
  releaseUnboundRows(store, root);
179
180
  const drained = await drainAs(store, globals, isFlagSet(DRAIN_CHILD_VARIABLE));
181
+ if (drained?.originRefusal) {
182
+ if (options.json)
183
+ printJsonError("records sync", { code: HARNESS_CODES.ORIGIN_MISMATCH, message: drained.originRefusal });
184
+ else {
185
+ process.stderr.write(`${drained.originRefusal}\n`);
186
+ process.exitCode = 1;
187
+ }
188
+ return;
189
+ }
180
190
  if (options.quiet)
181
191
  return;
182
192
  const counts = await countRecords(store);
@@ -2,6 +2,7 @@ import { ensureAccessToken } from "../lib/auth_grant.js";
2
2
  import { resolveNuStackUrl } from "../lib/nustack_client.js";
3
3
  import { makeReviewClient, ReviewUnreachableError } from "../lib/review/client.js";
4
4
  import { printJsonSuccess } from "../lib/json_output.js";
5
+ import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
5
6
  const EXIT_FOR = {
6
7
  signed: 0,
7
8
  refused: 1,
@@ -56,7 +57,7 @@ export async function runReviewCheck(options, globals) {
56
57
  process.exitCode = EXIT_FOR[state];
57
58
  };
58
59
  const baseUrl = resolveNuStackUrl(globals);
59
- const deviceAuth = await ensureAccessToken().catch(() => null);
60
+ const deviceAuth = await ensureAccessToken().catch(nullUnlessOriginRefused);
60
61
  if (!deviceAuth)
61
62
  return report("unreachable");
62
63
  try {
@@ -124,7 +125,7 @@ export async function runReviewMirror(options, globals) {
124
125
  if (!sessionId)
125
126
  return mirrorReport(json, "skipped", null, "--session is required");
126
127
  const baseUrl = resolveNuStackUrl(globals);
127
- const deviceAuth = await ensureAccessToken().catch(() => null);
128
+ const deviceAuth = await ensureAccessToken().catch(nullUnlessOriginRefused);
128
129
  if (!deviceAuth)
129
130
  return mirrorReport(json, "skipped", null, "signed out");
130
131
  const client = makeReviewClient(baseUrl, deviceAuth.accessToken);
@@ -5,17 +5,20 @@ import { ensureAccessToken } from "../lib/auth_grant.js";
5
5
  import { isInteractive } from "../lib/interactive.js";
6
6
  import { resolveNuStackUrl } from "../lib/nustack_client.js";
7
7
  import { makeSessionsClient, sessionsClientFromEnv } from "../lib/sessions/client.js";
8
+ import { mountProjectHarness } from "../lib/harness/mount.js";
8
9
  import { recordOutsideSessions, } from "../lib/sessions/outsideSessions.js";
9
10
  import { inspectSession, readOrphanedWorktrees, reclaimSession, } from "../lib/session-engine/prune.js";
10
11
  import { readSessionRegistry, sessionRegistryPath } from "../lib/session-engine/registry.js";
11
12
  import { SessionSupervisor } from "../lib/session-engine/supervisor.js";
12
13
  import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
14
+ import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
13
15
  export async function runSessionsServe(engineVersion) {
14
16
  const supervisor = new SessionSupervisor({
15
17
  input: process.stdin,
16
18
  output: process.stdout,
17
19
  engineVersion,
18
20
  sessions: sessionsClientFromEnv(process.env),
21
+ mountHarness: mountProjectHarness,
19
22
  });
20
23
  await supervisor.run();
21
24
  process.exit(0);
@@ -69,7 +72,7 @@ export async function runSessionsRecord(options, globals) {
69
72
  const transcripts = readTranscripts(payload?.sessions);
70
73
  if (transcripts.length === 0)
71
74
  return report("sessions record", EMPTY, json, "nothing to record");
72
- const deviceAuth = await ensureAccessToken().catch(() => null);
75
+ const deviceAuth = await ensureAccessToken().catch(nullUnlessOriginRefused);
73
76
  if (!deviceAuth)
74
77
  return report("sessions record", EMPTY, json, "signed out");
75
78
  const summary = await recordOutsideSessions(makeSessionsClient(baseUrl, deviceAuth.accessToken), {
@@ -6,6 +6,7 @@ import { ensureAccessToken, signedOutCopy } from "../lib/auth_grant.js";
6
6
  import { resolveProjectId } from "../lib/project_context.js";
7
7
  import { readManagedValues, writeManagedValues } from "../lib/env_file.js";
8
8
  import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
9
+ import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
9
10
  const COMMAND = "sync";
10
11
  const signedOut = () => signedOutCopy();
11
12
  export async function runSync(options) {
@@ -14,7 +15,7 @@ export async function runSync(options) {
14
15
  const json = options.json === true;
15
16
  if (!json)
16
17
  p.intro("nustack sync");
17
- const deviceAuth = await ensureAccessToken().catch(() => null);
18
+ const deviceAuth = await ensureAccessToken().catch(nullUnlessOriginRefused);
18
19
  if (!deviceAuth)
19
20
  return failSync(json, "SIGNED_OUT", signedOut(), "The managed block was not touched.");
20
21
  const accessToken = deviceAuth.accessToken;
package/dist/index.js CHANGED
@@ -5,6 +5,10 @@ if (command === "gate" && subcommand !== undefined && HOOK_SUBCOMMANDS.includes(
5
5
  const { runGateHook } = await import("./lib/gate/hook.js");
6
6
  await runGateHook(subcommand);
7
7
  }
8
+ else if (command === "check" && process.argv.includes("--hook")) {
9
+ const { runHookMode } = await import("./lib/harness/hookEntry.js");
10
+ await runHookMode(process.argv[process.argv.indexOf("--hook") + 1] ?? "", process.argv.includes("--protection"));
11
+ }
8
12
  else {
9
13
  await import("./program.js");
10
14
  }