@gtc6244/immediacy-daemon 0.1.0

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 (133) hide show
  1. package/dist/src/actions.d.ts +22 -0
  2. package/dist/src/actions.js +44 -0
  3. package/dist/src/actions.js.map +1 -0
  4. package/dist/src/agent/agent.d.ts +89 -0
  5. package/dist/src/agent/agent.js +22 -0
  6. package/dist/src/agent/agent.js.map +1 -0
  7. package/dist/src/agent/claude.d.ts +70 -0
  8. package/dist/src/agent/claude.js +404 -0
  9. package/dist/src/agent/claude.js.map +1 -0
  10. package/dist/src/agent/codex.d.ts +44 -0
  11. package/dist/src/agent/codex.js +303 -0
  12. package/dist/src/agent/codex.js.map +1 -0
  13. package/dist/src/agent/mock.d.ts +31 -0
  14. package/dist/src/agent/mock.js +155 -0
  15. package/dist/src/agent/mock.js.map +1 -0
  16. package/dist/src/agent/model-error.d.ts +8 -0
  17. package/dist/src/agent/model-error.js +25 -0
  18. package/dist/src/agent/model-error.js.map +1 -0
  19. package/dist/src/agent/openrouter.d.ts +38 -0
  20. package/dist/src/agent/openrouter.js +60 -0
  21. package/dist/src/agent/openrouter.js.map +1 -0
  22. package/dist/src/agent/workspace-scope.d.ts +25 -0
  23. package/dist/src/agent/workspace-scope.js +71 -0
  24. package/dist/src/agent/workspace-scope.js.map +1 -0
  25. package/dist/src/allowlist.d.ts +56 -0
  26. package/dist/src/allowlist.js +141 -0
  27. package/dist/src/allowlist.js.map +1 -0
  28. package/dist/src/chatlog.d.ts +50 -0
  29. package/dist/src/chatlog.js +179 -0
  30. package/dist/src/chatlog.js.map +1 -0
  31. package/dist/src/cli/paths.d.ts +40 -0
  32. package/dist/src/cli/paths.js +76 -0
  33. package/dist/src/cli/paths.js.map +1 -0
  34. package/dist/src/cli/service.d.ts +20 -0
  35. package/dist/src/cli/service.js +245 -0
  36. package/dist/src/cli/service.js.map +1 -0
  37. package/dist/src/cli.d.ts +2 -0
  38. package/dist/src/cli.js +227 -0
  39. package/dist/src/cli.js.map +1 -0
  40. package/dist/src/config.d.ts +33 -0
  41. package/dist/src/config.js +122 -0
  42. package/dist/src/config.js.map +1 -0
  43. package/dist/src/credentials.d.ts +38 -0
  44. package/dist/src/credentials.js +120 -0
  45. package/dist/src/credentials.js.map +1 -0
  46. package/dist/src/crypto.d.ts +49 -0
  47. package/dist/src/crypto.js +202 -0
  48. package/dist/src/crypto.js.map +1 -0
  49. package/dist/src/daemon-identity.d.ts +31 -0
  50. package/dist/src/daemon-identity.js +97 -0
  51. package/dist/src/daemon-identity.js.map +1 -0
  52. package/dist/src/envelope.d.ts +14 -0
  53. package/dist/src/envelope.js +28 -0
  54. package/dist/src/envelope.js.map +1 -0
  55. package/dist/src/events.d.ts +40 -0
  56. package/dist/src/events.js +24 -0
  57. package/dist/src/events.js.map +1 -0
  58. package/dist/src/forward.d.ts +51 -0
  59. package/dist/src/forward.js +170 -0
  60. package/dist/src/forward.js.map +1 -0
  61. package/dist/src/fs-scope.d.ts +17 -0
  62. package/dist/src/fs-scope.js +47 -0
  63. package/dist/src/fs-scope.js.map +1 -0
  64. package/dist/src/github.d.ts +70 -0
  65. package/dist/src/github.js +178 -0
  66. package/dist/src/github.js.map +1 -0
  67. package/dist/src/gitops.d.ts +113 -0
  68. package/dist/src/gitops.js +557 -0
  69. package/dist/src/gitops.js.map +1 -0
  70. package/dist/src/hub.d.ts +50 -0
  71. package/dist/src/hub.js +92 -0
  72. package/dist/src/hub.js.map +1 -0
  73. package/dist/src/index.d.ts +22 -0
  74. package/dist/src/index.js +56 -0
  75. package/dist/src/index.js.map +1 -0
  76. package/dist/src/jobs/scheduler.d.ts +94 -0
  77. package/dist/src/jobs/scheduler.js +373 -0
  78. package/dist/src/jobs/scheduler.js.map +1 -0
  79. package/dist/src/jobs/store.d.ts +33 -0
  80. package/dist/src/jobs/store.js +111 -0
  81. package/dist/src/jobs/store.js.map +1 -0
  82. package/dist/src/log.d.ts +61 -0
  83. package/dist/src/log.js +166 -0
  84. package/dist/src/log.js.map +1 -0
  85. package/dist/src/migrate-state-dir.d.ts +1 -0
  86. package/dist/src/migrate-state-dir.js +47 -0
  87. package/dist/src/migrate-state-dir.js.map +1 -0
  88. package/dist/src/models.d.ts +36 -0
  89. package/dist/src/models.js +71 -0
  90. package/dist/src/models.js.map +1 -0
  91. package/dist/src/permission-gate.d.ts +48 -0
  92. package/dist/src/permission-gate.js +93 -0
  93. package/dist/src/permission-gate.js.map +1 -0
  94. package/dist/src/permission-scope.d.ts +77 -0
  95. package/dist/src/permission-scope.js +244 -0
  96. package/dist/src/permission-scope.js.map +1 -0
  97. package/dist/src/ports.d.ts +71 -0
  98. package/dist/src/ports.js +216 -0
  99. package/dist/src/ports.js.map +1 -0
  100. package/dist/src/projects.d.ts +83 -0
  101. package/dist/src/projects.js +205 -0
  102. package/dist/src/projects.js.map +1 -0
  103. package/dist/src/protocol.d.ts +1226 -0
  104. package/dist/src/protocol.js +16 -0
  105. package/dist/src/protocol.js.map +1 -0
  106. package/dist/src/repo-sync.d.ts +49 -0
  107. package/dist/src/repo-sync.js +83 -0
  108. package/dist/src/repo-sync.js.map +1 -0
  109. package/dist/src/server.d.ts +33 -0
  110. package/dist/src/server.js +973 -0
  111. package/dist/src/server.js.map +1 -0
  112. package/dist/src/terminal.d.ts +32 -0
  113. package/dist/src/terminal.js +92 -0
  114. package/dist/src/terminal.js.map +1 -0
  115. package/dist/src/titler.d.ts +15 -0
  116. package/dist/src/titler.js +72 -0
  117. package/dist/src/titler.js.map +1 -0
  118. package/dist/src/types.d.ts +99 -0
  119. package/dist/src/types.js +3 -0
  120. package/dist/src/types.js.map +1 -0
  121. package/dist/src/usage-limit.d.ts +49 -0
  122. package/dist/src/usage-limit.js +83 -0
  123. package/dist/src/usage-limit.js.map +1 -0
  124. package/dist/src/usage.d.ts +23 -0
  125. package/dist/src/usage.js +63 -0
  126. package/dist/src/usage.js.map +1 -0
  127. package/dist/src/workspace.d.ts +303 -0
  128. package/dist/src/workspace.js +1086 -0
  129. package/dist/src/workspace.js.map +1 -0
  130. package/dist/src/worktree.d.ts +133 -0
  131. package/dist/src/worktree.js +561 -0
  132. package/dist/src/worktree.js.map +1 -0
  133. package/package.json +67 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Permission gate: holds deferred tool-call decisions until the phone answers.
3
+ *
4
+ * Shared by every agent backend. Default-deny on timeout (§5) so a backgrounded
5
+ * or disconnected phone can never leave a tool call hanging — or worse, let one
6
+ * through.
7
+ */
8
+ export class PermissionGate {
9
+ timeoutMs;
10
+ pending = new Map();
11
+ constructor(timeoutMs = 120_000) {
12
+ this.timeoutMs = timeoutMs;
13
+ }
14
+ /** Register a deferred request; resolves when the phone responds or times out. */
15
+ wait(requestId) {
16
+ return new Promise((resolve) => {
17
+ const timer = setTimeout(() => {
18
+ this.pending.delete(requestId);
19
+ resolve("deny"); // default-deny
20
+ }, this.timeoutMs);
21
+ // Don't let a pending prompt hold the process open.
22
+ timer.unref?.();
23
+ this.pending.set(requestId, { resolve, timer });
24
+ });
25
+ }
26
+ /** Resolve a pending request from a `permission.respond`. No-op if unknown. */
27
+ resolve(requestId, decision) {
28
+ const p = this.pending.get(requestId);
29
+ if (!p)
30
+ return false;
31
+ clearTimeout(p.timer);
32
+ this.pending.delete(requestId);
33
+ p.resolve(decision);
34
+ return true;
35
+ }
36
+ /** Deny everything outstanding (e.g. on agent stop). */
37
+ denyAll() {
38
+ for (const [, p] of this.pending) {
39
+ clearTimeout(p.timer);
40
+ p.resolve("deny");
41
+ }
42
+ this.pending.clear();
43
+ }
44
+ }
45
+ /**
46
+ * Question gate: the AskUserQuestion analogue of {@link PermissionGate}. Holds a
47
+ * deferred `AskUserQuestion` call until the user picks answers on some client.
48
+ *
49
+ * Two differences from the permission gate:
50
+ * - it resolves with the picked {@link QuestionResolution}, not allow/deny —
51
+ * those answers become the tool's result fed back to the model;
52
+ * - the timeout is long (a clarifying question is a deliberate ask the user may
53
+ * take minutes to answer). On timeout it resolves `null`, which the agent
54
+ * turns into a "no answer" denial so a disconnected client can't hang the
55
+ * SDK subprocess forever.
56
+ */
57
+ export class QuestionGate {
58
+ timeoutMs;
59
+ pending = new Map();
60
+ constructor(timeoutMs = 3_600_000) {
61
+ this.timeoutMs = timeoutMs;
62
+ }
63
+ /** Register a deferred question; resolves when the user answers or times out. */
64
+ wait(requestId) {
65
+ return new Promise((resolve) => {
66
+ const timer = setTimeout(() => {
67
+ this.pending.delete(requestId);
68
+ resolve(null); // default: no answer
69
+ }, this.timeoutMs);
70
+ timer.unref?.();
71
+ this.pending.set(requestId, { resolve, timer });
72
+ });
73
+ }
74
+ /** Resolve a pending question from a `question.respond`. No-op if unknown. */
75
+ resolve(requestId, resolution) {
76
+ const p = this.pending.get(requestId);
77
+ if (!p)
78
+ return false;
79
+ clearTimeout(p.timer);
80
+ this.pending.delete(requestId);
81
+ p.resolve(resolution);
82
+ return true;
83
+ }
84
+ /** Abandon everything outstanding (e.g. on agent stop) → treated as no answer. */
85
+ denyAll() {
86
+ for (const [, p] of this.pending) {
87
+ clearTimeout(p.timer);
88
+ p.resolve(null);
89
+ }
90
+ this.pending.clear();
91
+ }
92
+ }
93
+ //# sourceMappingURL=permission-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-gate.js","sourceRoot":"","sources":["../../src/permission-gate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Permission scoping: turn a single gated tool call into a reusable "allow all"
3
+ * rule, and test future calls against stored rules.
4
+ *
5
+ * This is the server-side half of the phone's "Allow all" button. When a client
6
+ * answers a permission request with `allow_all`, the daemon derives a {@link
7
+ * AllowRule} from that exact `(tool, input)` and persists it (see allowlist.ts,
8
+ * daemon-global + on disk). Every later call is checked against the stored rules
9
+ * *before* it defers to the phone — a match auto-approves, so "additional
10
+ * requests that fall into the same category" go through untouched.
11
+ *
12
+ * A rule is deliberately coarse — a *category*, not the exact call:
13
+ * - host — any web request to the same *site*: the registrable domain (eTLD+1)
14
+ * of the URL, so a grant made on docs.example.com also covers
15
+ * www./api./example.com (any page/path on any subdomain of the site).
16
+ * - dir — any file or shell call whose target resolves inside a directory
17
+ * (a file grant covers the folder it lives in; a Bash grant covers
18
+ * the directory the command runs in — its cwd, the worktree).
19
+ * - tool — any call of a tool we can't bound to a host or directory
20
+ * (MCP tools, WebSearch, unknown tools): allow that tool wholesale.
21
+ *
22
+ * Derivation is most-specific-first: a URL yields a host rule, a path (or a
23
+ * shell tool, scoped to its cwd) yields a dir rule, and everything else falls to
24
+ * a tool rule. Matching mirrors that, and is symlink-safe for dir rules (it goes
25
+ * through fs-scope's `isInside`), so a link inside a granted folder can't smuggle
26
+ * a call out of it.
27
+ */
28
+ export type AllowRule = {
29
+ kind: "host";
30
+ host: string;
31
+ } | {
32
+ kind: "dir";
33
+ path: string;
34
+ } | {
35
+ kind: "tool";
36
+ tool: string;
37
+ };
38
+ /**
39
+ * Derive the rule an "Allow all" on this call should create, or `null` when the
40
+ * call can't be generalized (e.g. a shape we don't model) — in which case the
41
+ * client's `allow_all` degrades to a plain one-shot allow.
42
+ */
43
+ export declare function deriveRule(tool: string, input: Record<string, unknown>, worktreePath: string): AllowRule | null;
44
+ /** True when any stored rule already covers this call. */
45
+ export declare function anyRuleMatches(rules: readonly AllowRule[], tool: string, input: Record<string, unknown>, worktreePath: string): boolean;
46
+ /** True when a single rule covers this call. */
47
+ export declare function ruleMatches(rule: AllowRule, tool: string, input: Record<string, unknown>, worktreePath: string): boolean;
48
+ /** Stable identity for dedup + storage; two calls in the same category share it. */
49
+ export declare function ruleKey(rule: AllowRule): string;
50
+ /** One-line human description for the log line and the admin list. */
51
+ export declare function describeRule(rule: AllowRule): string;
52
+ /** Coerce arbitrary parsed JSON into a valid AllowRule, or null if malformed. */
53
+ export declare function sanitizeRule(input: unknown): AllowRule | null;
54
+ /**
55
+ * Root URL — scheme + host (e.g. `https://example.com`) — of the first usable
56
+ * URL field, or null. Used for a human "web request to …" summary: the path,
57
+ * query, and JSON wrapping are dropped so the notification stays readable.
58
+ */
59
+ export declare function webRootUrl(input: Record<string, unknown>): string | null;
60
+ /**
61
+ * The registrable domain ("site") of a hostname: its public suffix plus one
62
+ * label — example.com, relay.link, foo.co.uk. Host rules are keyed on this so a
63
+ * grant made on any subdomain (docs./api./www.) covers the whole site, which is
64
+ * what a user means by "allow this site."
65
+ *
66
+ * This is a pragmatic approximation of the Public Suffix List, not the full
67
+ * list: the public suffix is taken as one label, except for a curated set of
68
+ * two-label suffixes (see {@link MULTI_PART_SUFFIXES}) where it is two — both
69
+ * common ccTLDs (co.uk, com.au) and popular *private* app-hosting suffixes
70
+ * (github.io, vercel.app, …) where each subdomain is a DIFFERENT owner, so
71
+ * folding to the bare suffix would let a grant on one user's site silently
72
+ * cover every site on the platform. An unknown two-label suffix still folds one
73
+ * label too far — that only ever *broadens* a grant (never narrows it), which is
74
+ * acceptable because grants are user-initiated and revocable. IP literals and
75
+ * single-label hosts (localhost, intranet names) are returned verbatim.
76
+ */
77
+ export declare function siteOf(host: string): string;
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Permission scoping: turn a single gated tool call into a reusable "allow all"
3
+ * rule, and test future calls against stored rules.
4
+ *
5
+ * This is the server-side half of the phone's "Allow all" button. When a client
6
+ * answers a permission request with `allow_all`, the daemon derives a {@link
7
+ * AllowRule} from that exact `(tool, input)` and persists it (see allowlist.ts,
8
+ * daemon-global + on disk). Every later call is checked against the stored rules
9
+ * *before* it defers to the phone — a match auto-approves, so "additional
10
+ * requests that fall into the same category" go through untouched.
11
+ *
12
+ * A rule is deliberately coarse — a *category*, not the exact call:
13
+ * - host — any web request to the same *site*: the registrable domain (eTLD+1)
14
+ * of the URL, so a grant made on docs.example.com also covers
15
+ * www./api./example.com (any page/path on any subdomain of the site).
16
+ * - dir — any file or shell call whose target resolves inside a directory
17
+ * (a file grant covers the folder it lives in; a Bash grant covers
18
+ * the directory the command runs in — its cwd, the worktree).
19
+ * - tool — any call of a tool we can't bound to a host or directory
20
+ * (MCP tools, WebSearch, unknown tools): allow that tool wholesale.
21
+ *
22
+ * Derivation is most-specific-first: a URL yields a host rule, a path (or a
23
+ * shell tool, scoped to its cwd) yields a dir rule, and everything else falls to
24
+ * a tool rule. Matching mirrors that, and is symlink-safe for dir rules (it goes
25
+ * through fs-scope's `isInside`), so a link inside a granted folder can't smuggle
26
+ * a call out of it.
27
+ */
28
+ import { dirname, isAbsolute, resolve } from "node:path";
29
+ import { isInside } from "./fs-scope.js";
30
+ /** Shell tools run with `cwd = worktreePath`; a grant scopes to that directory. */
31
+ const SHELL_TOOLS = new Set(["Bash", "BashOutput", "KillShell"]);
32
+ /** Input fields that name a filesystem target (mirrors workspace-scope.ts). */
33
+ const PATH_FIELDS = ["file_path", "path", "notebook_path"];
34
+ /** Input fields that may carry a URL (WebFetch + browser/MCP tools). */
35
+ const URL_FIELDS = ["url", "uri"];
36
+ /**
37
+ * Derive the rule an "Allow all" on this call should create, or `null` when the
38
+ * call can't be generalized (e.g. a shape we don't model) — in which case the
39
+ * client's `allow_all` degrades to a plain one-shot allow.
40
+ */
41
+ export function deriveRule(tool, input, worktreePath) {
42
+ const host = extractHost(input);
43
+ if (host)
44
+ return { kind: "host", host: siteOf(host) };
45
+ const dir = callDir(tool, input, worktreePath);
46
+ if (dir)
47
+ return { kind: "dir", path: dir };
48
+ // Nothing bound it to a host or a directory — allow the tool as a category.
49
+ return { kind: "tool", tool };
50
+ }
51
+ /** True when any stored rule already covers this call. */
52
+ export function anyRuleMatches(rules, tool, input, worktreePath) {
53
+ return rules.some((r) => ruleMatches(r, tool, input, worktreePath));
54
+ }
55
+ /** True when a single rule covers this call. */
56
+ export function ruleMatches(rule, tool, input, worktreePath) {
57
+ switch (rule.kind) {
58
+ case "host": {
59
+ const host = extractHost(input);
60
+ // Compare on the registrable domain, so any subdomain of the granted site
61
+ // matches (docs./api./www. all fold to the same eTLD+1).
62
+ return host !== null && siteOf(host) === rule.host;
63
+ }
64
+ case "dir": {
65
+ const dir = callDir(tool, input, worktreePath);
66
+ // Containment (not equality) so a grant on a folder also covers its
67
+ // subfolders; isInside canonicalizes symlinks on both sides.
68
+ return dir !== null && isInside(rule.path, dir);
69
+ }
70
+ case "tool":
71
+ return tool === rule.tool;
72
+ }
73
+ }
74
+ /** Stable identity for dedup + storage; two calls in the same category share it. */
75
+ export function ruleKey(rule) {
76
+ switch (rule.kind) {
77
+ case "host":
78
+ return `host:${rule.host}`;
79
+ case "dir":
80
+ return `dir:${rule.path}`;
81
+ case "tool":
82
+ return `tool:${rule.tool}`;
83
+ }
84
+ }
85
+ /** One-line human description for the log line and the admin list. */
86
+ export function describeRule(rule) {
87
+ switch (rule.kind) {
88
+ case "host":
89
+ return `all requests to ${rule.host}`;
90
+ case "dir":
91
+ return `all tool calls under ${rule.path}`;
92
+ case "tool":
93
+ return `all ${rule.tool} calls`;
94
+ }
95
+ }
96
+ /** Coerce arbitrary parsed JSON into a valid AllowRule, or null if malformed. */
97
+ export function sanitizeRule(input) {
98
+ if (!input || typeof input !== "object")
99
+ return null;
100
+ const o = input;
101
+ if (o.kind === "host" && typeof o.host === "string" && o.host.trim())
102
+ // Fold through siteOf so freshly-derived and legacy exact-host rules
103
+ // (persisted before subdomain coverage existed) normalize to the same
104
+ // registrable domain — an old "www.example.com" grant loads as
105
+ // "example.com" and keeps working.
106
+ return { kind: "host", host: siteOf(o.host.trim().toLowerCase()) };
107
+ if (o.kind === "dir" && typeof o.path === "string" && o.path.trim())
108
+ return { kind: "dir", path: resolve(o.path) };
109
+ if (o.kind === "tool" && typeof o.tool === "string" && o.tool.trim())
110
+ return { kind: "tool", tool: o.tool };
111
+ return null;
112
+ }
113
+ /**
114
+ * The directory a call targets, for dir-rule derivation/matching:
115
+ * - shell tools → their cwd (the worktree), since they run there;
116
+ * - path tools → the parent folder of the (worktree-resolved) target path;
117
+ * - anything else → null (not directory-scoped).
118
+ */
119
+ function callDir(tool, input, worktreePath) {
120
+ if (SHELL_TOOLS.has(tool))
121
+ return resolve(worktreePath);
122
+ const p = pathField(input);
123
+ if (p === null)
124
+ return null;
125
+ const abs = isAbsolute(p) ? resolve(p) : resolve(worktreePath, p);
126
+ return dirname(abs);
127
+ }
128
+ /** First usable (non-empty string) path field, else null. */
129
+ function pathField(input) {
130
+ for (const f of PATH_FIELDS) {
131
+ const v = input[f];
132
+ if (typeof v === "string" && v.trim().length > 0)
133
+ return v;
134
+ }
135
+ return null;
136
+ }
137
+ /**
138
+ * Root URL — scheme + host (e.g. `https://example.com`) — of the first usable
139
+ * URL field, or null. Used for a human "web request to …" summary: the path,
140
+ * query, and JSON wrapping are dropped so the notification stays readable.
141
+ */
142
+ export function webRootUrl(input) {
143
+ for (const f of URL_FIELDS) {
144
+ const v = input[f];
145
+ if (typeof v !== "string" || v.trim().length === 0)
146
+ continue;
147
+ try {
148
+ const u = new URL(v);
149
+ if (u.hostname)
150
+ return `${u.protocol}//${u.host}`;
151
+ }
152
+ catch {
153
+ /* not a parseable URL — fall through */
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ /** Lowercased hostname of the first usable URL field, or null. */
159
+ function extractHost(input) {
160
+ for (const f of URL_FIELDS) {
161
+ const v = input[f];
162
+ if (typeof v !== "string" || v.trim().length === 0)
163
+ continue;
164
+ try {
165
+ const host = new URL(v).hostname.toLowerCase();
166
+ if (host)
167
+ return host;
168
+ }
169
+ catch {
170
+ /* not a parseable URL — fall through */
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+ /**
176
+ * The registrable domain ("site") of a hostname: its public suffix plus one
177
+ * label — example.com, relay.link, foo.co.uk. Host rules are keyed on this so a
178
+ * grant made on any subdomain (docs./api./www.) covers the whole site, which is
179
+ * what a user means by "allow this site."
180
+ *
181
+ * This is a pragmatic approximation of the Public Suffix List, not the full
182
+ * list: the public suffix is taken as one label, except for a curated set of
183
+ * two-label suffixes (see {@link MULTI_PART_SUFFIXES}) where it is two — both
184
+ * common ccTLDs (co.uk, com.au) and popular *private* app-hosting suffixes
185
+ * (github.io, vercel.app, …) where each subdomain is a DIFFERENT owner, so
186
+ * folding to the bare suffix would let a grant on one user's site silently
187
+ * cover every site on the platform. An unknown two-label suffix still folds one
188
+ * label too far — that only ever *broadens* a grant (never narrows it), which is
189
+ * acceptable because grants are user-initiated and revocable. IP literals and
190
+ * single-label hosts (localhost, intranet names) are returned verbatim.
191
+ */
192
+ export function siteOf(host) {
193
+ // Drop a trailing-dot FQDN ("example.com.") so it folds like "example.com"
194
+ // instead of yielding a bare-suffix rule ("com.") that mis-scopes matches.
195
+ const h = host.endsWith(".") ? host.slice(0, -1) : host;
196
+ if (isIpLiteral(h) || !h.includes("."))
197
+ return h;
198
+ const labels = h.split(".");
199
+ const suffixLabels = MULTI_PART_SUFFIXES.has(labels.slice(-2).join(".")) ? 2 : 1;
200
+ const take = suffixLabels + 1; // registrable domain = public suffix + 1 label
201
+ return labels.length <= take ? h : labels.slice(-take).join(".");
202
+ }
203
+ /** True for an IPv4 literal or a bracketed IPv6 host — never reduce these. */
204
+ function isIpLiteral(host) {
205
+ return host.startsWith("[") || /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
206
+ }
207
+ /**
208
+ * Two-label suffixes treated as a single public suffix, so the label ABOVE them
209
+ * is the registrable domain — foo.co.uk folds to foo.co.uk (not co.uk), and
210
+ * alice.github.io folds to alice.github.io (not github.io). Two groups:
211
+ *
212
+ * - ccTLD second-levels (co.uk, com.au, …) — the classic PSL case.
213
+ * - private app-hosting suffixes (github.io, vercel.app, …) where each
214
+ * subdomain is a separate tenant; without these a grant on one project's
215
+ * site would auto-approve every site on the platform (a real trust widening
216
+ * for a permission allowlist).
217
+ *
218
+ * Not exhaustive — the frequent ones a coding agent actually fetches. Anything
219
+ * not listed folds one label too far, which only broadens (never narrows) a
220
+ * user-initiated, revocable grant. See siteOf for the tradeoff.
221
+ */
222
+ const MULTI_PART_SUFFIXES = new Set([
223
+ // ccTLD second-level domains
224
+ "co.uk", "org.uk", "gov.uk", "ac.uk", "me.uk", "net.uk", "sch.uk",
225
+ "com.au", "net.au", "org.au", "edu.au", "gov.au", "id.au",
226
+ "co.jp", "or.jp", "ne.jp", "ac.jp", "go.jp",
227
+ "co.nz", "org.nz", "net.nz", "govt.nz", "ac.nz",
228
+ "co.in", "net.in", "org.in", "gen.in", "firm.in", "ind.in",
229
+ "com.br", "net.br", "org.br", "gov.br",
230
+ "co.za", "org.za",
231
+ "com.cn", "net.cn", "org.cn", "gov.cn",
232
+ "co.kr", "or.kr",
233
+ "com.mx", "com.ar", "com.tr", "com.sg", "com.hk", "com.tw",
234
+ // Private app-hosting suffixes (per-subdomain tenancy — must NOT fold away)
235
+ "github.io", "gitlab.io", "githubusercontent.com",
236
+ "vercel.app", "netlify.app", "pages.dev", "workers.dev", "r2.dev",
237
+ "web.app", "firebaseapp.com", "appspot.com",
238
+ "herokuapp.com", "onrender.com", "fly.dev", "railway.app",
239
+ "azurewebsites.net", "cloudfront.net",
240
+ "blogspot.com", "wordpress.com", "readthedocs.io",
241
+ "repl.co", "replit.app", "glitch.me", "surge.sh", "ngrok.io",
242
+ "supabase.co",
243
+ ]);
244
+ //# sourceMappingURL=permission-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-scope.js","sourceRoot":"","sources":["../../src/permission-scope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Worktree-owned port discovery.
3
+ *
4
+ * When an agent runs `npm run dev` (or anything that binds a TCP port) inside
5
+ * its worktree, we want to surface that port to the operator so they can preview
6
+ * it in a local browser via a forwarded stream (see `forward.ts`).
7
+ *
8
+ * The authoritative signal is the OS, not the dev server's banner text: we ask
9
+ * `lsof` which processes are LISTENing on TCP, then correlate each listening
10
+ * process to a worktree by its working directory (`cwd`). A process whose `cwd`
11
+ * resolves inside a worktree path is attributed to that workspace — this catches
12
+ * servers launched in the interactive PTY and by the agent alike, and (because
13
+ * `cwd` survives it) a `npm run dev &` that gets reparented to launchd.
14
+ *
15
+ * The same attribution doubles as the security boundary: `forward.open` only
16
+ * dials ports that appear here, so a client cannot reach arbitrary loopback
17
+ * ports on the daemon box (the daemon's own WSS port, SSH, etc. never resolve to
18
+ * a worktree cwd, so they never appear).
19
+ */
20
+ /** A discovered port attributed to a workspace. */
21
+ export interface OwnedPort {
22
+ ws: string;
23
+ port: number;
24
+ addr: string;
25
+ }
26
+ /** What the scanner needs to know about live worktrees to attribute ports. */
27
+ export interface WorktreeRef {
28
+ ws: string;
29
+ worktreePath: string;
30
+ }
31
+ /** Parse `host:port` (incl. IPv6 `[::1]:port` / `*:port`) → {addr, port}. */
32
+ export declare function splitHostPort(name: string): {
33
+ addr: string;
34
+ port: number;
35
+ } | null;
36
+ /**
37
+ * One discovery pass: which worktree-owned ports are listening right now.
38
+ * Deduped by (ws, port); a port that binds both IPv4 and IPv6 collapses to one
39
+ * entry, preferring an explicit loopback address for display.
40
+ */
41
+ export declare function scanOwnedPorts(worktrees: WorktreeRef[]): Promise<OwnedPort[]>;
42
+ /**
43
+ * Polls `scanOwnedPorts` on an interval and fires `onChange` only when the set
44
+ * actually moves (a new server came up / one went away), so clients get an
45
+ * event on transitions rather than a steady heartbeat. Start it when at least
46
+ * one client is attached; stop it when the last one leaves.
47
+ */
48
+ export declare class PortScanner {
49
+ private worktrees;
50
+ private onChange;
51
+ private timer;
52
+ private last;
53
+ private scanning;
54
+ /** A refresh() that landed during an in-flight scan; honored before tick ends. */
55
+ private rescanRequested;
56
+ constructor(worktrees: () => WorktreeRef[], onChange: (ports: OwnedPort[]) => void);
57
+ /** The most recent scan result (used to answer `forward.list` immediately). */
58
+ current: OwnedPort[];
59
+ start(): void;
60
+ stop(): void;
61
+ /**
62
+ * Force an out-of-band scan (e.g. a client just attached, or an agent turn
63
+ * finished). Guaranteed to yield a scan that started after this call: if one
64
+ * is already running, `rescanRequested` makes the in-flight loop run again
65
+ * rather than dropping the request.
66
+ */
67
+ refresh(): Promise<void>;
68
+ private tick;
69
+ /** Is `port` currently owned by `ws`? The forward-open security check. */
70
+ isOwned(ws: string, port: number): boolean;
71
+ }