@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
@@ -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
  }
@@ -9,6 +9,9 @@ function managedEntry(matcher, command, options) {
9
9
  hook.timeout = options.timeout;
10
10
  return { matcher, hooks: [hook] };
11
11
  }
12
+ function isManagedCommand(command) {
13
+ return command.trimEnd().endsWith(`# ${MANAGED_MARKER}`);
14
+ }
12
15
  function carriesMarker(entry) {
13
16
  if (entry === null || typeof entry !== "object")
14
17
  return false;
@@ -18,7 +21,7 @@ function carriesMarker(entry) {
18
21
  return hooks.some((hook) => hook !== null &&
19
22
  typeof hook === "object" &&
20
23
  typeof hook.command === "string" &&
21
- hook.command.includes(MANAGED_MARKER));
24
+ isManagedCommand(hook.command));
22
25
  }
23
26
  function isManagedEntry(entry, matcher) {
24
27
  if (!carriesMarker(entry))
@@ -0,0 +1,55 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const PROBE_TIMEOUT_MS = 1500;
5
+ export function desktopConfigDir() {
6
+ return (process.env.NUSTACK_DESKTOP_CONFIG_DIR ||
7
+ path.join(os.homedir(), "Library", "Application Support", "co.nurix.nustack"));
8
+ }
9
+ export async function readServerRecord() {
10
+ const recordPath = path.join(desktopConfigDir(), "server.json");
11
+ try {
12
+ const parsed = JSON.parse(await readFile(recordPath, "utf8"));
13
+ if (typeof parsed.port !== "number" ||
14
+ typeof parsed.token !== "string" ||
15
+ typeof parsed.pid !== "number" ||
16
+ typeof parsed.startedAt !== "string") {
17
+ return null;
18
+ }
19
+ return parsed;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function isPidAlive(pid) {
26
+ try {
27
+ process.kill(pid, 0);
28
+ return true;
29
+ }
30
+ catch (error) {
31
+ return error.code === "EPERM";
32
+ }
33
+ }
34
+ export function isRosterProject(value) {
35
+ return (value !== null &&
36
+ typeof value === "object" &&
37
+ typeof value.path === "string" &&
38
+ typeof value.label === "string");
39
+ }
40
+ export async function probeProjects(record) {
41
+ try {
42
+ const response = await fetch(`http://127.0.0.1:${record.port}/api/projects`, {
43
+ headers: { Authorization: `Bearer ${record.token}` },
44
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
45
+ });
46
+ if (!response.ok)
47
+ return null;
48
+ const body = (await response.json());
49
+ const projects = body.data?.projects;
50
+ return Array.isArray(projects) ? projects.filter(isRosterProject) : null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
@@ -3,6 +3,7 @@ import { createHash, randomBytes } from "node:crypto";
3
3
  import { getOrCreateDeviceId } from "./device_identity.js";
4
4
  import { deleteDeviceCredential, readCredentialFailure, readDeviceCredential, writeDeviceCredential } from "./device_keychain.js";
5
5
  import { openLoopbackListener } from "./loopback.js";
6
+ import { originMismatchError, refusedContractOrigin } from "./origin_guard.js";
6
7
  const REQUEST_TIMEOUT_MS = 10_000;
7
8
  const BOOTSTRAP_TIMEOUT_MS = 25_000;
8
9
  const LOOPBACK_TIMEOUT_MS = 10 * 60_000;
@@ -160,11 +161,14 @@ export async function readStoredCredential() {
160
161
  credential: { deviceToken: credential.device_token, studioBaseUrl: credential.studio_base_url, deviceId, userEmail: credential.user_email },
161
162
  };
162
163
  }
163
- export async function resolveAccessToken() {
164
+ export async function resolveAccessToken(origin) {
164
165
  const fromEnv = envToken();
165
166
  if (fromEnv) {
166
167
  if (fromEnv.kind === "malformed")
167
168
  return { kind: "malformed_token", message: fromEnv.message };
169
+ const refused = refusedContractOrigin(null, origin);
170
+ if (refused)
171
+ return { kind: "origin_mismatch", error: originMismatchError(refused, null, "pat") };
168
172
  const deviceId = resolvedDeviceId ?? (await getOrCreateDeviceId());
169
173
  resolvedDeviceId = deviceId;
170
174
  return { kind: "ok", session: { accessToken: fromEnv.token, userEmail: "", deviceId } };
@@ -173,12 +177,20 @@ export async function resolveAccessToken() {
173
177
  if (!credential) {
174
178
  if (failure)
175
179
  lastDeadReason = failure;
180
+ const refused = refusedContractOrigin(null, origin);
181
+ if (refused)
182
+ return { kind: "origin_mismatch", error: originMismatchError(refused, null, "none") };
176
183
  return { kind: "none", reason: failure };
177
184
  }
185
+ const refused = refusedContractOrigin(credential.studioBaseUrl, origin);
186
+ if (refused)
187
+ return { kind: "origin_mismatch", error: originMismatchError(refused, credential.studioBaseUrl, "device") };
178
188
  return { kind: "ok", session: { accessToken: credential.deviceToken, userEmail: credential.userEmail, deviceId } };
179
189
  }
180
- export async function ensureAccessToken() {
181
- const outcome = await resolveAccessToken();
190
+ export async function ensureAccessToken(origin) {
191
+ const outcome = await resolveAccessToken(origin);
192
+ if (outcome.kind === "origin_mismatch")
193
+ throw outcome.error;
182
194
  return outcome.kind === "ok" ? { accessToken: outcome.session.accessToken, userEmail: outcome.session.userEmail } : null;
183
195
  }
184
196
  export async function forgetRefusedToken(staleToken) {
@@ -2,6 +2,7 @@ import * as p from "@clack/prompts";
2
2
  import { deadCredentialCopy, ensureAccessToken, resolveAccessToken } from "./auth_grant.js";
3
3
  import { performDeviceLogin } from "../commands/login.js";
4
4
  import { isInteractive } from "./interactive.js";
5
+ import { originMismatchError, refusedContractOrigin } from "./origin_guard.js";
5
6
  let lastFailure = { code: "SIGNED_OUT", message: deadCredentialCopy(null), retryable: false };
6
7
  export function lastAuthFailure() {
7
8
  return lastFailure;
@@ -12,6 +13,8 @@ function describeFailure(outcome) {
12
13
  }
13
14
  if (outcome.kind === "malformed_token")
14
15
  return { code: "TOKEN_MALFORMED", message: outcome.message, retryable: false };
16
+ if (outcome.kind === "origin_mismatch")
17
+ return { code: "ORIGIN_MISMATCH", message: outcome.error.message, retryable: false };
15
18
  return { code: "SIGNED_OUT", message: deadCredentialCopy(outcome.reason), retryable: false };
16
19
  }
17
20
  function reconnectingCopy(outcome) {
@@ -24,7 +27,7 @@ function reconnectingCopy(outcome) {
24
27
  export async function ensureAuthWithRecovery(baseUrl, json) {
25
28
  let outcome;
26
29
  try {
27
- outcome = await resolveAccessToken();
30
+ outcome = await resolveAccessToken(baseUrl);
28
31
  }
29
32
  catch {
30
33
  lastFailure = describeFailure("unreachable");
@@ -33,13 +36,17 @@ export async function ensureAuthWithRecovery(baseUrl, json) {
33
36
  if (outcome.kind === "ok")
34
37
  return { accessToken: outcome.session.accessToken, userEmail: outcome.session.userEmail };
35
38
  lastFailure = describeFailure(outcome);
36
- if (json || !isInteractive() || outcome.kind === "malformed_token")
39
+ if (json || !isInteractive() || outcome.kind === "malformed_token" || outcome.kind === "origin_mismatch")
37
40
  return null;
41
+ if (refusedContractOrigin(null, baseUrl)) {
42
+ lastFailure = describeFailure({ kind: "origin_mismatch", error: originMismatchError(baseUrl, null, "none") });
43
+ return null;
44
+ }
38
45
  p.log.warn(reconnectingCopy(outcome));
39
46
  const didLogin = await performDeviceLogin(baseUrl, process.cwd(), { reauthorize: true });
40
47
  if (!didLogin)
41
48
  return null;
42
- return ensureAccessToken().catch(() => {
49
+ return ensureAccessToken(baseUrl).catch(() => {
43
50
  lastFailure = describeFailure("unreachable");
44
51
  return null;
45
52
  });
@@ -2,7 +2,17 @@ import { detectPackageManager, installPackage } from "../package_manager.js";
2
2
  import { assertSafePackageName } from "../guards.js";
3
3
  import { updateBindingEtnaProfile } from "../local_store/bindings.js";
4
4
  import { serializeProfileForBinding } from "./profile.js";
5
- import { mountHarness } from "../harness/mount.js";
5
+ import { mountProjectHarness } from "../harness/mount.js";
6
+ export function profileNamesHarness(profile) {
7
+ return Boolean(profile.harnessDigest || profile.versionManifestPath || profile.manifestUrl);
8
+ }
9
+ export function mountReportOf(outcome) {
10
+ return { status: outcome.status, source: outcome.source, version: outcome.version, digest: outcome.digest };
11
+ }
12
+ export function mountFailureReport(error) {
13
+ const code = error?.code;
14
+ return { status: "failed", code: typeof code === "string" ? code : "HARNESS_MOUNT_FAILED", message: error instanceof Error ? error.message : String(error) };
15
+ }
6
16
  export function toolkitPackageName(profile) {
7
17
  if (!profile.name.startsWith("@"))
8
18
  return null;
@@ -16,14 +26,22 @@ export function toolkitPackageName(profile) {
16
26
  export async function installToolkit(store, bindingId, profile, cwd, deps = {}) {
17
27
  const packageName = toolkitPackageName(profile);
18
28
  let result;
19
- const manifestUrl = deps.manifestUrl ?? profile.manifestUrl ?? null;
20
- if (manifestUrl) {
29
+ if (deps.skipMount) {
30
+ result = { ok: true, installed: false, skipped: true, reason: "skipped_etna_mount", mount: { status: "skipped_etna_mount" } };
31
+ }
32
+ else if (profileNamesHarness(profile)) {
21
33
  try {
22
- await (deps.mount ?? mountHarness)(cwd, manifestUrl);
23
- result = { ok: true, installed: true, skipped: false, reason: "mounted" };
34
+ const outcome = await (deps.mount ?? mountProjectHarness)(cwd, {
35
+ projectId: deps.projectId,
36
+ globals: deps.globals,
37
+ writeContract: deps.writeContract ?? "never",
38
+ runtime: "claude-code",
39
+ });
40
+ result = { ok: true, installed: true, skipped: false, reason: "mounted", mount: mountReportOf(outcome) };
24
41
  }
25
42
  catch (error) {
26
- result = { ok: false, installed: false, skipped: false, reason: error instanceof Error ? error.message : String(error) };
43
+ const mount = mountFailureReport(error);
44
+ result = { ok: false, installed: false, skipped: false, reason: mount.message, mount };
27
45
  }
28
46
  }
29
47
  else if (!packageName) {
@@ -1,4 +1,5 @@
1
1
  import { getProject } from "../nustack_client.js";
2
+ import { contractFieldsFromSummary } from "../project_contract.js";
2
3
  export class ToolkitProfileUnavailableError extends Error {
3
4
  projectId;
4
5
  constructor(projectId) {
@@ -21,6 +22,11 @@ export async function fetchToolkitProfile(baseUrl, accessToken, projectId) {
21
22
  catalogVersionId: null,
22
23
  harnessVersion: project.harness?.version ?? null,
23
24
  manifestUrl: project.harness?.manifestUrl ?? null,
25
+ harnessTrack: project.harness?.track ?? null,
26
+ harnessDigest: project.harness?.digest ?? null,
27
+ versionManifestPath: project.harness?.versionManifestPath ?? null,
28
+ branchPolicy: project.branchPolicy === "trunk" || project.branchPolicy === "branch" ? project.branchPolicy : null,
29
+ contractFields: contractFieldsFromSummary(project),
24
30
  };
25
31
  }
26
32
  export function toolkitProfileFromManifest(manifest) {
@@ -22,6 +22,12 @@ export function parseRecordedToolkit(binding) {
22
22
  export function toolkitCompatibility(mounted, cloud) {
23
23
  if (!mounted || !cloud)
24
24
  return { compatible: true, blocksWrites: false, reason: null };
25
+ if (mounted.digest && cloud.harnessDigest) {
26
+ if (mounted.digest !== cloud.harnessDigest) {
27
+ return { compatible: false, blocksWrites: true, reason: `mounted harness digest ${mounted.digest.slice(0, 12)} differs from the expected ${cloud.harnessDigest.slice(0, 12)}` };
28
+ }
29
+ return { compatible: true, blocksWrites: false, reason: null };
30
+ }
25
31
  const pinned = cloud.harnessVersion ?? cloud.version;
26
32
  if (mounted.version !== pinned) {
27
33
  return { compatible: false, blocksWrites: true, reason: `mounted harness ${mounted.version} differs from the cloud pin ${pinned}` };