javi-forge 1.38.6 → 1.39.1

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 (38) hide show
  1. package/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +61 -3
  2. package/assets/claude-hooks/javi-forge-windows-secure-object.ps1 +29 -6
  3. package/assets/claude-hooks/manifest.json +4 -3
  4. package/assets/preparation-worker.c +180 -0
  5. package/dist/cli/dispatch/doctor.d.ts +3 -0
  6. package/dist/cli/dispatch/doctor.js +27 -0
  7. package/dist/cli/dispatch/simple-renderers.d.ts +1 -1
  8. package/dist/cli/dispatch/simple-renderers.js +3 -3
  9. package/dist/cli/help.d.ts +5 -1
  10. package/dist/cli/help.js +2 -0
  11. package/dist/cli/main.js +26 -5
  12. package/dist/commands/doctor.d.ts +7 -2
  13. package/dist/commands/doctor.js +32 -13
  14. package/dist/commands/plugin.js +18 -6
  15. package/dist/lib/__fixtures__/fake-secure-fs.js +6 -3
  16. package/dist/lib/agent-skills.d.ts +2 -1
  17. package/dist/lib/agent-skills.js +36 -22
  18. package/dist/lib/plugin-replacement.d.ts +28 -0
  19. package/dist/lib/plugin-replacement.js +170 -0
  20. package/dist/lib/plugin.d.ts +2 -1
  21. package/dist/lib/plugin.js +26 -21
  22. package/dist/lib/preparation-authorization.d.ts +31 -0
  23. package/dist/lib/preparation-authorization.js +134 -0
  24. package/dist/lib/preparation-capability.d.ts +64 -0
  25. package/dist/lib/preparation-capability.js +106 -0
  26. package/dist/lib/preparation-executor.d.ts +49 -0
  27. package/dist/lib/preparation-executor.js +427 -0
  28. package/dist/lib/preparation-stager.d.ts +25 -0
  29. package/dist/lib/preparation-stager.js +207 -0
  30. package/dist/lib/preparation-status-ok.d.ts +36 -0
  31. package/dist/lib/preparation-status-ok.js +161 -0
  32. package/dist/lib/secure-fs-posix.js +7 -2
  33. package/dist/lib/secure-fs-transaction.d.ts +12 -2
  34. package/dist/lib/secure-fs-transaction.js +62 -14
  35. package/dist/lib/secure-fs-windows.js +9 -1
  36. package/dist/ui/Doctor.d.ts +7 -1
  37. package/dist/ui/Doctor.js +3 -24
  38. package/package.json +1 -1
@@ -792,12 +792,63 @@ function bashSubstitutions(command) {
792
792
  }
793
793
  return bodies;
794
794
  }
795
+ // This deliberately recognizes only a direct, quoted Python stdin heredoc on
796
+ // the first command line. It removes the opaque body before shell inspection;
797
+ // it does not parse Python or permit its execution.
798
+ function pythonQuotedHeredoc(command) {
799
+ const newline = command.indexOf("\n");
800
+ const header = newline < 0 ? command : command.slice(0, newline);
801
+ const starts = [0];
802
+ let quote = "", escaped = false, heredocs = 0;
803
+ for (let index = 0; index < header.length; index++) {
804
+ const char = header[index];
805
+ if (escaped) { escaped = false; continue; }
806
+ if (char === "\\" && quote !== "'") { escaped = true; continue; }
807
+ if (quote) { if (char === quote) quote = ""; continue; }
808
+ if (char === "'" || char === '"' || char === "`") { quote = char; continue; }
809
+ if (char === "<" && header[index + 1] === "<") { heredocs++; index++; }
810
+ if (char === ";" || char === "|" || char === "&") starts.push(index + 1);
811
+ }
812
+ const literalPath = String.raw`(?:[A-Za-z0-9_./~-]+|'[A-Za-z0-9_./~ -]+'|"[A-Za-z0-9_./~ -]+")`;
813
+ const redirection = String.raw`(?:[0-9]*>>?|[0-9]*<)[ \t]*${literalPath}[ \t]*`;
814
+ const python = String.raw`[ \t]*(?:python3?|/(?:[A-Za-z0-9_.-]+/)*python3?)(?:[ \t]+-)?[ \t]*`;
815
+ const pattern = new RegExp(String.raw`^(${python}(?:${redirection})*)<<(-?)[ \t]*(['"])([A-Za-z_][A-Za-z0-9_]*)\3([ \t]*(?:${redirection})*)([;|&].*)?$`);
816
+ for (const start of starts) {
817
+ const match = pattern.exec(header.slice(start));
818
+ if (!match) continue;
819
+ // Multiple documents and tab-stripping need a shell grammar this guard
820
+ // intentionally does not implement, so retain fail-closed behavior.
821
+ if (newline < 0 || heredocs !== 1 || match[2]) fail("unlexable-command");
822
+ if (match[6]?.trimStart().startsWith("|")) fail("unlexable-command");
823
+ let offset = newline + 1;
824
+ while (offset <= command.length) {
825
+ const end = command.indexOf("\n", offset);
826
+ const lineEnd = end < 0 ? command.length : end;
827
+ if (command.slice(offset, lineEnd) === match[4]) {
828
+ const outerHeader = header.slice(0, start) + match[1] + match[5] + (match[6] ?? "");
829
+ return { command: `${outerHeader}\n${command.slice(lineEnd + 1)}`, unsupported: true };
830
+ }
831
+ if (end < 0) break;
832
+ offset = end + 1;
833
+ }
834
+ fail("unlexable-command");
835
+ }
836
+ return { command, unsupported: false };
837
+ }
795
838
  function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT, depth = 0) {
796
839
  if (depth > 4) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
840
+ let unsupported = false;
841
+ try { ({ command, unsupported } = pythonQuotedHeredoc(command)); } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
797
842
  const headerOnlyCommand = opaqueInertCatHereDoc(command, cwd, config, projectRoot);
798
843
  if (headerOnlyCommand === null) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
799
844
  command = headerOnlyCommand;
800
- try { for (const body of bashSubstitutions(command)) { const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1); if (!nested.allowed) return nested; } } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
845
+ try {
846
+ for (const body of bashSubstitutions(command)) {
847
+ const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1);
848
+ if (nested.ruleId === "shell.unsupported-interpreter") unsupported = true;
849
+ else if (!nested.allowed) return nested;
850
+ }
851
+ } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
801
852
  if (/^\s*:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:\s*$/.test(command)) return { allowed: false, ruleId: "shell.destructive-root" };
802
853
  let parsed;
803
854
  try { parsed = lex(command); } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
@@ -834,10 +885,16 @@ function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot =
834
885
  if (/^(?:powershell|pwsh)(?:\.exe)?$/i.test(executable) && tokens.some((token) => /^-(?:enc|encodedcommand)$/i.test(token))) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
835
886
  if (/^(?:bash|sh|zsh|dash|ksh)$/.test(executable)) {
836
887
  const flag = tokens.findIndex((token) => /^-[^-]*c[^-]*$/.test(token));
837
- if (flag >= 0) { const body = tokens[flag + 1]; if (!body || /\$(?!\()/.test(body)) return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1); if (!nested.allowed) return nested; }
888
+ if (flag >= 0) {
889
+ const body = tokens[flag + 1];
890
+ if (!body || /\$(?!\()/.test(body)) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
891
+ const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1);
892
+ if (nested.ruleId === "shell.unsupported-interpreter") unsupported = true;
893
+ else if (!nested.allowed) return nested;
894
+ }
838
895
  }
839
896
  }
840
- return { allowed: true };
897
+ return unsupported ? { allowed: false, ruleId: "shell.unsupported-interpreter" } : { allowed: true };
841
898
  }
842
899
  function evaluatePowerShell(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT) {
843
900
  const parsed = lex(command, true);
@@ -939,6 +996,7 @@ function diagnostic(error) {
939
996
  }
940
997
  function denialDiagnostic(toolName, decision) {
941
998
  const tool = SUPPORTED_TOOLS.includes(toolName) ? toolName : "supported tool";
999
+ if (decision.ruleId === "shell.unsupported-interpreter") return "javi-forge PreToolUse denied Bash [shell.unsupported-interpreter]: quoted Python heredoc execution is unsupported";
942
1000
  if (decision.ambiguity) return `javi-forge PreToolUse denied ${tool} [${decision.ruleId}]: ${decision.ambiguity.utility} ${decision.ambiguity.profile} ${decision.ambiguity.sink} semantics denied as ambiguous`;
943
1001
  return `javi-forge PreToolUse denied ${tool} [${decision.ruleId}]: global guard policy denied the invocation`;
944
1002
  }
@@ -92,6 +92,7 @@ namespace JaviForge
92
92
  private const uint WRITE_OWNER_A = 0x00080000;
93
93
  private const uint DELETE_A = 0x00010000;
94
94
  private const uint FILE_READ_ATTRIBUTES = 0x0080;
95
+ private const uint DIRECTORY_FLUSH_ACCESS = GENERIC_WRITE | READ_CONTROL | FILE_READ_ATTRIBUTES;
95
96
 
96
97
  private const uint FILE_SHARE_READ = 0x1;
97
98
  private const uint FILE_SHARE_WRITE = 0x2;
@@ -866,12 +867,34 @@ namespace JaviForge
866
867
  // No opaque re-check here (unlike unlink/rmdir): both endpoints are our
867
868
  // own just-created nodes under the held parent-chain lock, so no on-path
868
869
  // node can be swapped while the chain handle is held.
869
- if (!MoveFileExW(fromFull, toFull,
870
- MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
871
- return OpResult.Fail(R_CHAIN, "rename failed " + Marshal.GetLastWin32Error()
872
- + " " + fromFull);
873
- FlushFileBuffers(dir.Handle);
874
- return OpResult.Good();
870
+ int flushErr;
871
+ IntPtr flushHandle = OpenNoFollow(dir.Path, DIRECTORY_FLUSH_ACCESS,
872
+ FILE_SHARE_READ | FILE_SHARE_WRITE, out flushErr);
873
+ if (flushHandle == INVALID_HANDLE)
874
+ return OpResult.Fail(R_CHAIN, "rename flush-open failed " + flushErr + " " + dir.Path);
875
+ try
876
+ {
877
+ uint attr; string opaque; bool zeroId;
878
+ if (!ReadInfo(flushHandle, out attr, out opaque, out zeroId))
879
+ return OpResult.Fail(R_CHAIN, "rename flush-open info failed " + dir.Path);
880
+ if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
881
+ return OpResult.Fail(R_CHAIN, "rename flush-open reparse point " + dir.Path);
882
+ if ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0)
883
+ return OpResult.Fail(R_CHAIN, "rename flush-open not a directory " + dir.Path);
884
+ if (zeroId)
885
+ return OpResult.Fail(R_CHAIN, "rename flush-open unresolvable identity " + dir.Path);
886
+ if (!String.Equals(opaque, dir.Opaque, StringComparison.OrdinalIgnoreCase))
887
+ return OpResult.Fail(R_CHAIN, "rename flush-open identity changed " + dir.Path);
888
+ if (!MoveFileExW(fromFull, toFull,
889
+ MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
890
+ return OpResult.Fail(R_CHAIN, "rename failed " + Marshal.GetLastWin32Error()
891
+ + " " + fromFull);
892
+ if (!FlushFileBuffers(flushHandle))
893
+ return OpResult.Fail(R_CHAIN, "rename flush failed " + Marshal.GetLastWin32Error()
894
+ + " " + dir.Path);
895
+ return OpResult.Good();
896
+ }
897
+ finally { CloseHandle(flushHandle); }
875
898
  }
876
899
  catch (Exception ex) { return OpResult.Fail(R_CHAIN, "rename exception " + ex.Message); }
877
900
  }
@@ -4,7 +4,7 @@
4
4
  "name": "javi-forge-skillguard-pre-tool-use.mjs",
5
5
  "version": 1,
6
6
  "policyVersion": 2,
7
- "sha256": "6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af",
7
+ "sha256": "507a57a15f1b967103bb1eefe701a74813d5a9603fa7bcc15579a215b03621e3",
8
8
  "historical": [
9
9
  "78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc",
10
10
  "5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425",
@@ -12,7 +12,8 @@
12
12
  "3581862f0567cce75a58b693c9ade80d39ee7d58add11537a34a8461c47c1ed4",
13
13
  "54a270f28b068450b79547a88ec6f2d4854514392fd5f38ed1d6174ea093d7aa",
14
14
  "9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e",
15
- "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1"
15
+ "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1",
16
+ "6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af"
16
17
  ]
17
18
  },
18
19
  "settingsEntries": {
@@ -30,7 +31,7 @@
30
31
  "installerHelpers": {
31
32
  "windowsSecureObject": {
32
33
  "name": "javi-forge-windows-secure-object.ps1",
33
- "sha256": "2289ef6ac6b039ec74dc3ea0894413e243ff9bea963f04008a356b3838f9b8dd"
34
+ "sha256": "4ee446d4e540adbea88c343df540efdcad1a5b26d31481ac5ae7057d8375cd11"
34
35
  }
35
36
  }
36
37
  }
@@ -0,0 +1,180 @@
1
+ /* Fixed Linux x86-64 data-only preparation worker. Build statically; never run
2
+ * generated helpers. Operator approves the exact binary AND this source digest.
3
+ * JSON checks are artifact validation, not helper behavior or domain-schema tests. */
4
+ #define _GNU_SOURCE
5
+ #include <errno.h>
6
+ #include <fcntl.h>
7
+ #include <linux/audit.h>
8
+ #include <linux/filter.h>
9
+ #include <linux/seccomp.h>
10
+ #include <stddef.h>
11
+ #include <stdint.h>
12
+ #include <stdio.h>
13
+ #include <stdlib.h>
14
+ #include <string.h>
15
+ #include <sys/prctl.h>
16
+ #include <sys/socket.h>
17
+ #include <sys/stat.h>
18
+ #include <sys/syscall.h>
19
+ #include <time.h>
20
+ #include <unistd.h>
21
+
22
+ #define LIMIT 1048576
23
+ #define DIR_FLAGS (O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC)
24
+ #define FILE_FLAGS (O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC)
25
+ static const char *names[6] = {"app-request.json", "minimal.py", "test_minimal.py", "dispatch.py", "run_gateway.py", "preparation-result.json"};
26
+ static unsigned char *data[6];
27
+ static size_t sizes[6];
28
+ extern char **environ;
29
+
30
+ #define ALLOW(n) BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_##n, 0, 1), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
31
+ static int restrict_syscalls(void) {
32
+ struct sock_filter rules[] = {
33
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
34
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
35
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
36
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
37
+ ALLOW(read), ALLOW(write), ALLOW(close), ALLOW(fstat), ALLOW(newfstatat),
38
+ ALLOW(fsync), ALLOW(fchmod), ALLOW(mkdirat), ALLOW(unlinkat),
39
+ ALLOW(brk), ALLOW(mmap), ALLOW(munmap), ALLOW(mprotect), ALLOW(futex),
40
+ ALLOW(clock_gettime), ALLOW(alarm), ALLOW(rt_sigaction), ALLOW(rt_sigprocmask),
41
+ ALLOW(rt_sigreturn), ALLOW(exit), ALLOW(exit_group), ALLOW(getpid),
42
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 1, 0),
43
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
44
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])),
45
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, DIR_FLAGS, 2, 0),
46
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, FILE_FLAGS, 1, 0),
47
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
48
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
49
+ };
50
+ struct sock_fprog program = {(unsigned short)(sizeof(rules) / sizeof(rules[0])), rules};
51
+ return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program);
52
+ }
53
+ static int isolation_probes(int parent) {
54
+ errno = 0;
55
+ if (syscall(__NR_socket, AF_INET, SOCK_STREAM, 0) != -1 || errno != EPERM) return 0;
56
+ errno = 0;
57
+ if (openat(parent, "credential-probe", O_RDONLY) != -1 || errno != EPERM) return 0;
58
+ errno = 0;
59
+ if (syscall(__NR_clone, 0, 0, 0, 0, 0) != -1 || errno != EPERM) return 0;
60
+ errno = 0;
61
+ if (syscall(__NR_execve, "/worker", 0, 0) != -1 || errno != EPERM) return 0;
62
+ return 1;
63
+ }
64
+
65
+ struct json { const unsigned char *p, *end; };
66
+ static void ws(struct json *j) { while (j->p < j->end && (*j->p == ' ' || *j->p == '\n' || *j->p == '\r' || *j->p == '\t')) j->p++; }
67
+ static int hex(unsigned char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
68
+ static int string(struct json *j) {
69
+ if (j->p == j->end || *j->p++ != '"') return 0;
70
+ while (j->p < j->end) {
71
+ unsigned char c = *j->p++;
72
+ if (c == '"') return 1;
73
+ if (c < 32) return 0;
74
+ if (c == '\\') {
75
+ if (j->p == j->end) return 0;
76
+ c = *j->p++;
77
+ if (c == 'u') {
78
+ for (int i = 0; i < 4; i++) if (j->p == j->end || !hex(*j->p++)) return 0;
79
+ } else if (!strchr("\"\\/bfnrt", c)) return 0;
80
+ } else if (c >= 128) {
81
+ unsigned int value; int count;
82
+ if (c >= 0xc2 && c <= 0xdf) { value = c & 31; count = 1; }
83
+ else if (c >= 0xe0 && c <= 0xef) { value = c & 15; count = 2; }
84
+ else if (c >= 0xf0 && c <= 0xf4) { value = c & 7; count = 3; }
85
+ else return 0;
86
+ int remaining = count;
87
+ while (remaining--) { if (j->p == j->end || (*j->p & 0xc0) != 0x80) return 0; value = (value << 6) | (*j->p++ & 63); }
88
+ if ((count == 2 && value < 0x800) || (count == 3 && value < 0x10000) || value > 0x10ffff || (value >= 0xd800 && value <= 0xdfff)) return 0;
89
+ }
90
+ }
91
+ return 0;
92
+ }
93
+ static int value(struct json *j, int depth) {
94
+ ws(j); if (depth > 64 || j->p == j->end) return 0;
95
+ unsigned char c = *j->p;
96
+ if (c == '"') return string(j);
97
+ if (c == '{' || c == '[') {
98
+ unsigned char stop = c == '{' ? '}' : ']'; j->p++; ws(j);
99
+ if (j->p < j->end && *j->p == stop) { j->p++; return 1; }
100
+ for (;;) {
101
+ if (c == '{') { if (!string(j)) return 0; ws(j); if (j->p == j->end || *j->p++ != ':') return 0; }
102
+ if (!value(j, depth + 1)) return 0;
103
+ ws(j); if (j->p == j->end) return 0;
104
+ unsigned char next = *j->p++; if (next == stop) return 1;
105
+ if (next != ',') return 0;
106
+ ws(j);
107
+ }
108
+ }
109
+ const char *literal = c == 't' ? "true" : c == 'f' ? "false" : c == 'n' ? "null" : NULL;
110
+ if (literal) { size_t n = strlen(literal); if ((size_t)(j->end - j->p) < n || memcmp(j->p, literal, n)) return 0; j->p += n; return 1; }
111
+ if (*j->p == '-') j->p++;
112
+ if (j->p == j->end) return 0;
113
+ if (*j->p == '0') j->p++;
114
+ else { if (*j->p < '1' || *j->p > '9') return 0; do { j->p++; } while (j->p < j->end && *j->p >= '0' && *j->p <= '9'); }
115
+ if (j->p < j->end && *j->p == '.') { j->p++; const unsigned char *start = j->p; while (j->p < j->end && *j->p >= '0' && *j->p <= '9') j->p++; if (j->p == start) return 0; }
116
+ if (j->p < j->end && (*j->p == 'e' || *j->p == 'E')) { j->p++; if (j->p < j->end && (*j->p == '+' || *j->p == '-')) j->p++; const unsigned char *start = j->p; while (j->p < j->end && *j->p >= '0' && *j->p <= '9') j->p++; if (j->p == start) return 0; }
117
+ return 1;
118
+ }
119
+ static int object(const unsigned char *bytes, size_t n) { struct json j = {bytes, bytes + n}; ws(&j); if (j.p == j.end || *j.p != '{' || !value(&j, 0)) return 0; ws(&j); return j.p == j.end; }
120
+ static int write_all(int fd, const unsigned char *bytes, size_t length) {
121
+ while (length) { ssize_t n = write(fd, bytes, length); if (n <= 0) return 0; bytes += n; length -= (size_t)n; } return 1;
122
+ }
123
+
124
+ int main(int argc, char **argv) {
125
+ (void)argv;
126
+ alarm(30); umask(077); setvbuf(stdout, NULL, _IONBF, 0);
127
+ if (argc != 1) return 11;
128
+ if (environ && *environ && (environ[1] || strcmp(environ[0], "PWD=/"))) return 12;
129
+ if (clearenv()) return 13;
130
+ struct timespec started; if (clock_gettime(CLOCK_MONOTONIC, &started)) return 10;
131
+ int parent = open("/out", DIR_FLAGS); struct stat parent_stat, existing;
132
+ if (parent < 0 || fstat(parent, &parent_stat) || (parent_stat.st_mode & 0777) != 0700) return 10;
133
+ if (fstatat(parent, "attempt-3", &existing, AT_SYMLINK_NOFOLLOW) == 0 || errno != ENOENT) return 10;
134
+ unsigned long long dev, ino; FILE *identity = fopen("/identity", "r");
135
+ if (!identity || fscanf(identity, "%llu %llu", &dev, &ino) != 2 || dev != (unsigned long long)parent_stat.st_dev || ino != (unsigned long long)parent_stat.st_ino) return 10;
136
+ fclose(identity);
137
+ size_t total = 0;
138
+ for (int i = 0; i < 6; i++) {
139
+ char filename[128]; snprintf(filename, sizeof(filename), "/payload/%s", names[i]);
140
+ int fd = open(filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); struct stat st;
141
+ if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 || st.st_size > LIMIT) return 10;
142
+ sizes[i] = (size_t)st.st_size; total += sizes[i]; if (total > LIMIT) return 10;
143
+ data[i] = malloc(sizes[i] + 1); if (!data[i]) return 10;
144
+ size_t offset = 0;
145
+ while (offset < sizes[i]) { ssize_t n = read(fd, data[i] + offset, sizes[i] - offset); if (n <= 0) return 10; offset += (size_t)n; }
146
+ close(fd); data[i][sizes[i]] = 0;
147
+ }
148
+ if (restrict_syscalls() || !isolation_probes(parent)) return 10;
149
+ #ifdef PREPARATION_FIXTURE_NOT_READY
150
+ for (;;) { /* Test-only readiness stall, never a production build. */ }
151
+ #endif
152
+ puts("READY");
153
+ char go; if (read(STDIN_FILENO, &go, 1) != 1 || go != 'G') return 10;
154
+ alarm(10);
155
+ #ifdef PREPARATION_FIXTURE_STALL
156
+ for (;;) { /* Test-only compiled variant, approved only by fixture keys. */ }
157
+ #endif
158
+ if (!object(data[0], sizes[0]) || !object(data[5], sizes[5])) return 20;
159
+ for (int i = 1; i < 5; i++) if (!sizes[i] || memchr(data[i], 0, sizes[i])) return 20;
160
+ puts("VALID");
161
+ struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now)) return 20;
162
+ long elapsed = now.tv_sec - started.tv_sec; if (elapsed >= 30) return 20;
163
+ alarm((unsigned int)(30 - elapsed));
164
+ if (mkdirat(parent, "attempt-3", 0700)) return 30;
165
+ int dir = openat(parent, "attempt-3", DIR_FLAGS); struct stat directory_stat;
166
+ if (dir < 0 || fstat(dir, &directory_stat)) return 30;
167
+ printf("DIR %llu %llu\n", (unsigned long long)directory_stat.st_dev, (unsigned long long)directory_stat.st_ino);
168
+ for (int i = 0; i < 6; i++) {
169
+ int fd = openat(dir, names[i], FILE_FLAGS, 0600); struct stat st;
170
+ if (fd < 0 || fstat(fd, &st)) return 30;
171
+ printf("FILE %d %llu %llu\n", i, (unsigned long long)st.st_dev, (unsigned long long)st.st_ino);
172
+ if (fchmod(fd, 0600) || !write_all(fd, data[i], sizes[i]) || fsync(fd)) return 30;
173
+ close(fd);
174
+ #ifdef PREPARATION_FIXTURE_PARTIAL
175
+ if (i == 0) return 30;
176
+ #endif
177
+ }
178
+ if (fsync(dir) || fsync(parent)) return 30;
179
+ close(dir); close(parent); puts("DONE"); return 0;
180
+ }
@@ -0,0 +1,3 @@
1
+ import { type DoctorOptions } from "../../commands/doctor.js";
2
+ export default function DoctorController({ dryRun, refreshContext, }: DoctorOptions): import("react").FunctionComponentElement<import("../../ui/Doctor.js").DoctorProps>;
3
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { createElement, useEffect, useState } from "react";
2
+ import { runDoctor } from "../../commands/doctor.js";
3
+ import Doctor from "../../ui/Doctor.js";
4
+ export default function DoctorController({ dryRun = false, refreshContext = false, }) {
5
+ const [result, setResult] = useState(null);
6
+ const [error, setError] = useState(null);
7
+ const [loading, setLoading] = useState(true);
8
+ const runCheck = () => {
9
+ setLoading(true);
10
+ setResult(null);
11
+ setError(null);
12
+ void runDoctor(undefined, { dryRun, refreshContext })
13
+ .then((nextResult) => {
14
+ setResult(nextResult);
15
+ setLoading(false);
16
+ })
17
+ .catch((nextError) => {
18
+ setError(String(nextError));
19
+ setLoading(false);
20
+ });
21
+ };
22
+ useEffect(() => {
23
+ runCheck();
24
+ }, [dryRun, refreshContext]);
25
+ return createElement(Doctor, { loading, result, error, onRerun: runCheck });
26
+ }
27
+ //# sourceMappingURL=doctor.js.map
@@ -15,7 +15,7 @@ export interface RendererDeps {
15
15
  error?: (message: string) => void;
16
16
  setExitCode?: (code: number) => void;
17
17
  }
18
- export declare function handleDoctor(_cli: CLI, ctx: RendererCtx, deps?: RendererDeps): void;
18
+ export declare function handleDoctor(cli: CLI, ctx: RendererCtx, deps?: RendererDeps): void;
19
19
  export declare function handleAnalyze(cli: CLI, ctx: RendererCtx): void;
20
20
  export declare function handleLlmsTxt(cli: CLI, ctx: RendererCtx): void;
21
21
  export declare function handlePlugin(cli: CLI, ctx: RendererCtx): void;
@@ -13,11 +13,11 @@ import { resolvePlatformSupport } from "../../lib/platform-support.js";
13
13
  import AnalyzeUI from "../../ui/AnalyzeUI.js";
14
14
  import App from "../../ui/App.js";
15
15
  import { CIProvider as CIContextProvider } from "../../ui/CIContext.js";
16
- import Doctor from "../../ui/Doctor.js";
17
16
  import LlmsTxt from "../../ui/LlmsTxt.js";
18
17
  import Plugin from "../../ui/Plugin.js";
19
18
  import { VALID_CI, VALID_MEMORY, VALID_STACKS } from "../validators.js";
20
- export function handleDoctor(_cli, ctx, deps = {}) {
19
+ import DoctorController from "./doctor.js";
20
+ export function handleDoctor(cli, ctx, deps = {}) {
21
21
  const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
22
22
  if (platformSupport) {
23
23
  (deps.error ?? console.error)(`${platformSupport.refusalCode}: ${platformSupport.guidance}`);
@@ -28,7 +28,7 @@ export function handleDoctor(_cli, ctx, deps = {}) {
28
28
  return;
29
29
  }
30
30
  (deps.render ?? render)(React.createElement(CIContextProvider, { isCI: ctx.isCI },
31
- React.createElement(Doctor, null)), { stdin: ctx.inkStdin });
31
+ React.createElement(DoctorController, { dryRun: cli.flags.dryRun === true, refreshContext: cli.flags.refreshContext === true })), { stdin: ctx.inkStdin });
32
32
  }
33
33
  export function handleAnalyze(cli, ctx) {
34
34
  render(React.createElement(CIContextProvider, { isCI: ctx.isCI },
@@ -8,7 +8,7 @@
8
8
  * Help banner shown by meow when `--help` is passed or invalid args are supplied.
9
9
  * Multi-line template literal — preserve exact formatting (whitespace is significant).
10
10
  */
11
- export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n SkillGuard install gate (plugin add / plugin import / skills auto)\n Every install is scanned before anything is written. Refusals are\n fail-closed and name the offending files:\n - SKILL.md files that block (critical threats) are refused \u2014 always.\n - Unscannable files (binary, oversized, unreadable) are refused unless\n --force is given.\n - Symlinks anywhere in the tree and SKILL.md files outside the declared\n set are manifest-integrity refusals \u2014 they are refused even with --force.\n - Empty or missing skills.json `skills` array on import is refused.\n A refused install/auto-install exits non-zero (exit 1) so scripts and CI\n can tell a refusal apart from success; clean installs \u2014 including\n --force-lifted unscannable ones \u2014 exit 0.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge plugin add org/repo\n $ javi-forge plugin add org/repo --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
11
+ export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --refresh-context Refresh .context/ during doctor (writes INDEX.md, summary.md, manifest timestamp)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n SkillGuard install gate (plugin add / plugin import / skills auto)\n Every install is scanned before anything is written. Refusals are\n fail-closed and name the offending files:\n - SKILL.md files that block (critical threats) are refused \u2014 always.\n - Unscannable files (binary, oversized, unreadable) are refused unless\n --force is given.\n - Symlinks anywhere in the tree and SKILL.md files outside the declared\n set are manifest-integrity refusals \u2014 they are refused even with --force.\n - Empty or missing skills.json `skills` array on import is refused.\n A refused install/auto-install exits non-zero (exit 1) so scripts and CI\n can tell a refusal apart from success; clean installs \u2014 including\n --force-lifted unscannable ones \u2014 exit 0.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge plugin add org/repo\n $ javi-forge plugin add org/repo --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
12
12
  /**
13
13
  * Per-command help for `ci`, shown by `javi-forge ci --help` (or when `ci` is
14
14
  * given an unknown subcommand). Kept consistent with the global HELP_TEXT
@@ -30,6 +30,10 @@ export declare const FLAGS_SCHEMA: {
30
30
  readonly type: "boolean";
31
31
  readonly default: false;
32
32
  };
33
+ readonly refreshContext: {
34
+ readonly type: "boolean";
35
+ readonly default: false;
36
+ };
33
37
  readonly stack: {
34
38
  readonly type: "string";
35
39
  readonly default: "";
package/dist/cli/help.js CHANGED
@@ -59,6 +59,7 @@ export const HELP_TEXT = `
59
59
  --mock Enable mock-first mode (no real API keys needed)
60
60
  --local-ai Include local AI dev stack (Ollama + Docker Compose)
61
61
  --batch Non-interactive mode (auto-proceed, no keyboard input)
62
+ --refresh-context Refresh .context/ during doctor (writes INDEX.md, summary.md, manifest timestamp)
62
63
  --deep Enable deep analysis (conflict + duplicate detection)
63
64
  --budget, -b Token budget limit for skills (default: 8000)
64
65
  --skills-dir Custom skills directory path
@@ -214,6 +215,7 @@ export const FLAGS_SCHEMA = {
214
215
  // `ci --help` can show ci-specific usage instead of the global banner).
215
216
  help: { type: "boolean", shortFlag: "h", default: false },
216
217
  dryRun: { type: "boolean", default: false },
218
+ refreshContext: { type: "boolean", default: false },
217
219
  stack: { type: "string", default: "" },
218
220
  ci: { type: "string", default: "" },
219
221
  memory: { type: "string", default: "" },
package/dist/cli/main.js CHANGED
@@ -10,11 +10,21 @@ import { handleTdd } from "./dispatch/tdd.js";
10
10
  import { handleWorkflow } from "./dispatch/workflow.js";
11
11
  import { FLAGS_SCHEMA, HELP_TEXT } from "./help.js";
12
12
  import { createInkStdin, detectCI, setupUpdateNotifier } from "./runtime.js";
13
+ const KNOWN_COMMANDS = new Set([
14
+ "init",
15
+ "tdd",
16
+ "ci",
17
+ "hooks",
18
+ "doctor",
19
+ "analyze",
20
+ "workflow",
21
+ "llms-txt",
22
+ "plugin",
23
+ "skills",
24
+ "skill",
25
+ "security",
26
+ ]);
13
27
  export async function runCli() {
14
- // Check for updates in background (non-blocking, cached 24h)
15
- const _require = createRequire(import.meta.url);
16
- const pkg = _require("../../package.json");
17
- setupUpdateNotifier(pkg);
18
28
  const cli = meow(HELP_TEXT, {
19
29
  importMeta: import.meta,
20
30
  flags: FLAGS_SCHEMA,
@@ -23,12 +33,23 @@ export async function runCli() {
23
33
  autoHelp: false,
24
34
  });
25
35
  const subcommand = cli.input[0] ?? "init";
36
+ if (!KNOWN_COMMANDS.has(subcommand)) {
37
+ console.error(`Unknown command "${subcommand}". Run javi-forge --help for usage.`);
38
+ process.exit(1);
39
+ }
26
40
  // Global --help: every command except `ci` and `hooks` shows the global banner
27
41
  // here. Those two own their per-command help inside their handlers.
28
42
  if (cli.flags.help && subcommand !== "ci" && subcommand !== "hooks") {
29
43
  console.log(HELP_TEXT);
30
44
  process.exit(0);
31
45
  }
46
+ // Check for updates in background (non-blocking, cached 24h). Diagnostics and
47
+ // dry-runs must not create notifier cache/state before doing their actual work.
48
+ if (subcommand !== "doctor" && !cli.flags.dryRun) {
49
+ const _require = createRequire(import.meta.url);
50
+ const pkg = _require("../../package.json");
51
+ setupUpdateNotifier(pkg);
52
+ }
32
53
  const isCI = detectCI(cli.flags);
33
54
  const inkStdin = createInkStdin();
34
55
  switch (subcommand) {
@@ -76,7 +97,7 @@ export async function runCli() {
76
97
  await handleSecurity(cli);
77
98
  break;
78
99
  }
79
- default: {
100
+ case "init": {
80
101
  handleInitDefault(cli, { inkStdin, isCI });
81
102
  break;
82
103
  }
@@ -1,12 +1,16 @@
1
1
  import fs from "fs-extra";
2
2
  import { detectStack } from "../lib/common.js";
3
- import { refreshContextDir } from "../lib/context.js";
3
+ import { detectDependenciesDetailed, refreshContextDir } from "../lib/context.js";
4
4
  import { execFileAsync } from "../lib/exec.js";
5
5
  import { listInstalledPlugins } from "../lib/plugin.js";
6
6
  import type { DoctorResult } from "../types/index.js";
7
7
  export type CheckStatus = "ok" | "fail" | "skip";
8
8
  type DoctorFilesystem = Pick<typeof fs, "pathExists" | "readJson" | "readdir">;
9
- export interface DoctorDeps {
9
+ export interface DoctorOptions {
10
+ dryRun?: boolean;
11
+ refreshContext?: boolean;
12
+ }
13
+ export interface DoctorDeps extends DoctorOptions {
10
14
  platform?: string;
11
15
  cwd?: () => string;
12
16
  filesystem?: DoctorFilesystem;
@@ -14,6 +18,7 @@ export interface DoctorDeps {
14
18
  stackDetector?: typeof detectStack;
15
19
  pluginLister?: typeof listInstalledPlugins;
16
20
  contextRefresher?: typeof refreshContextDir;
21
+ dependencyDetector?: typeof detectDependenciesDetailed;
17
22
  }
18
23
  interface RemoteInfo {
19
24
  host: "github" | "gitlab" | "other";