@integrity-labs/agt-cli 0.28.848 → 0.28.850

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 (23) hide show
  1. package/dist/bin/agt.js +5 -5
  2. package/dist/{chunk-N4TMATK2.js → chunk-OQSPURDB.js} +4 -4
  3. package/dist/{chunk-MFU2FD4N.js → chunk-WUXQVP7A.js} +17 -5
  4. package/dist/{chunk-MFU2FD4N.js.map → chunk-WUXQVP7A.js.map} +1 -1
  5. package/dist/{chunk-PMQNTU7W.js → chunk-XCAI3GSF.js} +2 -2
  6. package/dist/{claude-pair-runtime-BQ5JUW5F.js → claude-pair-runtime-CJXQTVSC.js} +2 -2
  7. package/dist/lib/manager-worker.js +13 -13
  8. package/dist/mcp/computer-use-proxy.js +317 -4
  9. package/dist/mcp/direct-chat-channel.js +16 -4
  10. package/dist/mcp/index.js +58 -8
  11. package/dist/mcp/origami.js +16 -4
  12. package/dist/mcp/slack-channel.js +16 -4
  13. package/dist/mcp/telegram-channel.js +16 -4
  14. package/dist/{persistent-session-JKES3FMW.js → persistent-session-YGK7YHPR.js} +3 -3
  15. package/dist/{responsiveness-probe-P7EAWN6J.js → responsiveness-probe-NGWO3JK3.js} +3 -3
  16. package/dist/{session-auth-dead-IFU2XLHO.js → session-auth-dead-4UZR5655.js} +2 -2
  17. package/package.json +1 -1
  18. /package/dist/{chunk-N4TMATK2.js.map → chunk-OQSPURDB.js.map} +0 -0
  19. /package/dist/{chunk-PMQNTU7W.js.map → chunk-XCAI3GSF.js.map} +0 -0
  20. /package/dist/{claude-pair-runtime-BQ5JUW5F.js.map → claude-pair-runtime-CJXQTVSC.js.map} +0 -0
  21. /package/dist/{persistent-session-JKES3FMW.js.map → persistent-session-YGK7YHPR.js.map} +0 -0
  22. /package/dist/{responsiveness-probe-P7EAWN6J.js.map → responsiveness-probe-NGWO3JK3.js.map} +0 -0
  23. /package/dist/{session-auth-dead-IFU2XLHO.js.map → session-auth-dead-4UZR5655.js.map} +0 -0
@@ -18,7 +18,7 @@ import {
18
18
  rotateDailySession,
19
19
  sessionFileExists,
20
20
  todayLocalIso
21
- } from "./chunk-MFU2FD4N.js";
21
+ } from "./chunk-WUXQVP7A.js";
22
22
  import {
23
23
  reapOrphanChannelMcps
24
24
  } from "./chunk-XWVM4KPK.js";
@@ -5431,4 +5431,4 @@ export {
5431
5431
  stopAllSessionsAndWait,
5432
5432
  getProjectDir
5433
5433
  };
5434
- //# sourceMappingURL=chunk-PMQNTU7W.js.map
5434
+ //# sourceMappingURL=chunk-XCAI3GSF.js.map
@@ -100,7 +100,7 @@ async function spawnPairSession(session) {
100
100
  return { ok: true };
101
101
  } catch {
102
102
  }
103
- const { resolveClaudeBinary } = await import("./persistent-session-JKES3FMW.js");
103
+ const { resolveClaudeBinary } = await import("./persistent-session-YGK7YHPR.js");
104
104
  const claudeBin = resolveClaudeBinary();
105
105
  const pairEnv = {
106
106
  ...process.env,
@@ -373,4 +373,4 @@ export {
373
373
  startClaudePair,
374
374
  submitClaudePairCode
375
375
  };
376
- //# sourceMappingURL=claude-pair-runtime-BQ5JUW5F.js.map
376
+ //# sourceMappingURL=claude-pair-runtime-CJXQTVSC.js.map
@@ -60,7 +60,7 @@ import {
60
60
  safeWriteJsonAtomic,
61
61
  setConfigHash,
62
62
  tripClass
63
- } from "../chunk-N4TMATK2.js";
63
+ } from "../chunk-OQSPURDB.js";
64
64
  import {
65
65
  getProjectDir as getProjectDir2,
66
66
  getReadyTasks,
@@ -126,7 +126,7 @@ import {
126
126
  takeZombieDetection,
127
127
  toOpencodeModel,
128
128
  writeEgressAllowlist
129
- } from "../chunk-PMQNTU7W.js";
129
+ } from "../chunk-XCAI3GSF.js";
130
130
  import {
131
131
  ACCOUNT_ENFORCEMENT_MARKER_FILENAME,
132
132
  AnchorSessionClient,
@@ -221,7 +221,7 @@ import {
221
221
  subagentActivityAgeSeconds,
222
222
  sumTranscriptUsageInWindow,
223
223
  transcriptActivityAgeSeconds
224
- } from "../chunk-MFU2FD4N.js";
224
+ } from "../chunk-WUXQVP7A.js";
225
225
  import {
226
226
  reapOrphanChannelMcps
227
227
  } from "../chunk-XWVM4KPK.js";
@@ -12996,7 +12996,7 @@ var pendingDayRolloverReset = /* @__PURE__ */ new Set();
12996
12996
  var dayRolloverInboundHold = /* @__PURE__ */ new Map();
12997
12997
  var INBOUND_HOLD_EPISODE_GAP_MS = 12e4;
12998
12998
  async function channelInboundActivityAgeSecondsFor(codeName) {
12999
- const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-P7EAWN6J.js");
12999
+ const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-NGWO3JK3.js");
13000
13000
  const newest = newestPendingInboundActivityMtimeMs(dirname11(paneLogPath(codeName)));
13001
13001
  if (newest === null) return null;
13002
13002
  return Math.max(0, Math.floor((Date.now() - newest) / 1e3));
@@ -13664,7 +13664,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
13664
13664
  var lastVersionCheckAt = 0;
13665
13665
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
13666
13666
  var lastResponsivenessProbeAt = 0;
13667
- var agtCliVersion = true ? "0.28.848" : "dev";
13667
+ var agtCliVersion = true ? "0.28.850" : "dev";
13668
13668
  function resolveBrewPath(execFileSync2) {
13669
13669
  try {
13670
13670
  const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -15254,7 +15254,7 @@ function flushRestartedAgentDiagnostics(hostId, codeNames) {
15254
15254
  if (codeNames.length === 0) return;
15255
15255
  void (async () => {
15256
15256
  try {
15257
- const { collectDiagnostics } = await import("../persistent-session-JKES3FMW.js");
15257
+ const { collectDiagnostics } = await import("../persistent-session-YGK7YHPR.js");
15258
15258
  await api.post("/host/heartbeat", {
15259
15259
  host_id: hostId,
15260
15260
  agent_diagnostics: collectDiagnostics(codeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor)
@@ -15396,7 +15396,7 @@ async function pollCycleInner() {
15396
15396
  }
15397
15397
  try {
15398
15398
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
15399
- const { collectDiagnostics } = await import("../persistent-session-JKES3FMW.js");
15399
+ const { collectDiagnostics } = await import("../persistent-session-YGK7YHPR.js");
15400
15400
  const diagCodeNames = [...agentState.persistentSessionAgents];
15401
15401
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor) : void 0;
15402
15402
  let tailscaleHostname;
@@ -15565,7 +15565,7 @@ async function pollCycleInner() {
15565
15565
  collectPanelessActivityProbes,
15566
15566
  getResponsivenessIntervalMs,
15567
15567
  occupancyQualificationClassifications
15568
- } = await import("../responsiveness-probe-P7EAWN6J.js");
15568
+ } = await import("../responsiveness-probe-NGWO3JK3.js");
15569
15569
  const probeIntervalMs = getResponsivenessIntervalMs();
15570
15570
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
15571
15571
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -15682,7 +15682,7 @@ async function pollCycleInner() {
15682
15682
  collectResponsivenessProbes,
15683
15683
  livePendingInboundOldestAgeSeconds,
15684
15684
  parkPendingInbound
15685
- } = await import("../responsiveness-probe-P7EAWN6J.js");
15685
+ } = await import("../responsiveness-probe-NGWO3JK3.js");
15686
15686
  const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-NDP36U7O.js");
15687
15687
  const wedgeNow = /* @__PURE__ */ new Date();
15688
15688
  const liveAgents = agentState.persistentSessionAgents;
@@ -15838,7 +15838,7 @@ async function pollCycleInner() {
15838
15838
  }
15839
15839
  try {
15840
15840
  const { scrapeMcpFailedBannerCount } = await import("../pane-mcp-banner-scraper-JA437JIB.js");
15841
- const { probeSessionAuth } = await import("../session-auth-dead-IFU2XLHO.js");
15841
+ const { probeSessionAuth } = await import("../session-auth-dead-4UZR5655.js");
15842
15842
  const observations = [];
15843
15843
  const pendingCacheCommits = [];
15844
15844
  const modelApiErrorReportingOn = hostFlagStore().getBoolean("model-api-error-reporting");
@@ -20071,7 +20071,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
20071
20071
  void api.post("/host/restart-ack", { host_id: hostId, agent_id: agentId, restart_requested_at: requestedAt }).catch((err) => log(`[restart-lane] ack failed for '${codeName}': ${err.message}`));
20072
20072
  void (async () => {
20073
20073
  try {
20074
- const { collectDiagnostics } = await import("../persistent-session-JKES3FMW.js");
20074
+ const { collectDiagnostics } = await import("../persistent-session-YGK7YHPR.js");
20075
20075
  await api.post("/host/heartbeat", {
20076
20076
  host_id: hostId,
20077
20077
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor)
@@ -20117,7 +20117,7 @@ async function respawnAgentAfterMcpStop(codeName, reason) {
20117
20117
  }
20118
20118
  try {
20119
20119
  const hostId = await getHostId();
20120
- const { collectDiagnostics } = await import("../persistent-session-JKES3FMW.js");
20120
+ const { collectDiagnostics } = await import("../persistent-session-YGK7YHPR.js");
20121
20121
  await api.post("/host/heartbeat", {
20122
20122
  host_id: hostId,
20123
20123
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor)
@@ -20753,7 +20753,7 @@ async function processClaudePairSessions(agents) {
20753
20753
  killPairSession,
20754
20754
  pairTmuxSession,
20755
20755
  finalizeClaudePairOnboarding
20756
- } = await import("../claude-pair-runtime-BQ5JUW5F.js");
20756
+ } = await import("../claude-pair-runtime-CJXQTVSC.js");
20757
20757
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
20758
20758
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
20759
20759
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -7,6 +7,303 @@ import { createInterface } from "readline";
7
7
  import { spawn } from "child_process";
8
8
  import { fileURLToPath } from "url";
9
9
  import { resolve as resolvePath } from "path";
10
+
11
+ // src/computer-use-gate.ts
12
+ import { createHash } from "crypto";
13
+ var MINIMUM_TOOL_FLOOR = "write_high_risk";
14
+ var TOOL_FLOORS = Object.freeze({
15
+ // Reads - of the operator's screen and window state.
16
+ get_app_state: "write_high_risk",
17
+ // State-changing: focus, input, navigation.
18
+ open_app: "write_high_risk",
19
+ scroll: "write_high_risk",
20
+ click: "write_destructive",
21
+ type_text: "write_destructive",
22
+ press_key: "write_destructive",
23
+ set_value: "write_destructive",
24
+ perform_secondary_action: "write_destructive"
25
+ });
26
+ var UNGATED_TOOLS = Object.freeze(["list_apps"]);
27
+ var APP_ARG_KEYS = ["app_name", "app", "application", "bundle_id"];
28
+ var TEXT_PREVIEW_LIMIT = 200;
29
+ var SESSION_PATH = "/host/computer-use/session";
30
+ var ACTION_PATH = "/host/computer-use/action";
31
+ function refuse(text) {
32
+ return { content: [{ type: "text", text }], isError: true };
33
+ }
34
+ function previewText(value, limit = TEXT_PREVIEW_LIMIT) {
35
+ if (typeof value !== "string" || value.length === 0) return void 0;
36
+ if (value.length <= limit) return value;
37
+ return `${value.slice(0, limit)}... (${value.length - limit} more characters)`;
38
+ }
39
+ function extractAppIdentifiers(args) {
40
+ if (typeof args !== "object" || args === null) return [];
41
+ const rec = args;
42
+ const found = [];
43
+ for (const key of APP_ARG_KEYS) {
44
+ const v = rec[key];
45
+ if (typeof v !== "string" || !v.trim()) continue;
46
+ const named = v.trim();
47
+ if (!found.some((f) => sameApp(f, named))) found.push(named);
48
+ }
49
+ return found;
50
+ }
51
+ function sameApp(a, b) {
52
+ if (!a || !b) return false;
53
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
54
+ }
55
+ function actionIdempotencyKey(sessionRequestId, tool, args) {
56
+ const canonical = `${sessionRequestId} ${tool} ${stableStringify(args)}`;
57
+ const digest = createHash("sha256").update(canonical).digest("hex").slice(0, 32);
58
+ return `cu-${sessionRequestId}-${tool}-${digest}`;
59
+ }
60
+ function stableStringify(value) {
61
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
62
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
63
+ const rec = value;
64
+ const keys = Object.keys(rec).sort();
65
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(rec[k])}`).join(",")}}`;
66
+ }
67
+ function createApprovalGate(deps) {
68
+ const logErr2 = deps.logErr ?? ((msg) => process.stderr.write(`[computer-use-gate] ${msg}
69
+ `));
70
+ return async (toolName, args, ctx) => {
71
+ if (UNGATED_TOOLS.includes(toolName)) return null;
72
+ const floor = TOOL_FLOORS[toolName];
73
+ if (!floor) {
74
+ return refuse(
75
+ `computer-use: '${toolName || "(unnamed)"}' has no declared approval floor, so it is refused. Every tool this proxy forwards must be classified at '${MINIMUM_TOOL_FLOOR}' or above (packages/mcp/src/computer-use-gate.ts TOOL_FLOORS). Tell your operator which tool you needed.`
76
+ );
77
+ }
78
+ const appIds = extractAppIdentifiers(args);
79
+ if (appIds.length > 1) {
80
+ return refuse(
81
+ `computer-use: this call names more than one application (${appIds.map((a) => `'${a}'`).join(", ")}), so it was NOT sent to the Mac. A grant covers ONE application, and when the arguments disagree this proxy cannot tell which one the Mac would actually act on - so it refuses rather than guessing. Re-send the call naming the target exactly once.`
82
+ );
83
+ }
84
+ const requestedApp = appIds.length === 1 ? appIds[0] : void 0;
85
+ let grant;
86
+ try {
87
+ grant = await deps.apiGet(
88
+ `${SESSION_PATH}?agent_id=${encodeURIComponent(deps.agentId)}`
89
+ );
90
+ } catch (err) {
91
+ logErr2(`session lookup failed: ${err.message}`);
92
+ return refuse(
93
+ `computer-use: cannot reach the control plane to check your session, so this call was NOT sent to the Mac. This is not a denial and not a Mac problem - approval could not be checked, so nothing ran. Retry shortly.`
94
+ );
95
+ }
96
+ async function requestSession(lead) {
97
+ if (!requestedApp) {
98
+ return refuse(
99
+ `${lead} This call also does not name an application, so a session cannot be requested for it. Call list_apps first, then retry naming the app.`
100
+ );
101
+ }
102
+ try {
103
+ const filed = await deps.apiPost(SESSION_PATH, {
104
+ agent_id: deps.agentId,
105
+ target_app: requestedApp,
106
+ summary: `Drive ${requestedApp} on the operator's Mac (first call: ${toolName})`,
107
+ idempotency_key: `cu-session-${deps.agentId}-${requestedApp.toLowerCase()}`
108
+ });
109
+ return refuse(
110
+ `${lead} A session request for '${requestedApp}' has been filed` + (filed?.request_id ? ` (request ${filed.request_id})` : "") + `. A human will approve or deny it and choose whether you are prompted for each action. Poll with check_approval; once it is active, re-issue this call.`
111
+ );
112
+ } catch (err) {
113
+ logErr2(`session request failed: ${err.message}`);
114
+ return refuse(
115
+ `${lead} The request for a session could not be filed (${err.message}), so nothing is pending either. Tell your operator.`
116
+ );
117
+ }
118
+ }
119
+ switch (grant.state) {
120
+ case "active":
121
+ break;
122
+ case "pending":
123
+ return refuse(
124
+ `computer-use: your session request is still awaiting a human decision` + (grant.request_id ? ` (request ${grant.request_id})` : "") + `. Nothing has been sent to the Mac. Do not re-request - poll with check_approval, or do something else and come back.`
125
+ );
126
+ case "revoked":
127
+ return refuse(
128
+ `computer-use: your session was REVOKED by a human before it expired. Do not request another for the same task without asking them first - a revocation mid-session usually means they wanted you to stop, not to retry.`
129
+ );
130
+ case "denied":
131
+ return refuse(
132
+ `computer-use: a human DENIED your session request` + (grant.denial_reason ? `: ${grant.denial_reason}` : ".") + ` Do not retry the same request. Ask them what they would approve instead.`
133
+ );
134
+ case "expired":
135
+ return requestSession(
136
+ `computer-use: your session has EXPIRED - a human did grant it, and the window simply ran out. Nothing was sent to the Mac.`
137
+ );
138
+ case "none":
139
+ default:
140
+ return requestSession(
141
+ `computer-use: you have no active session, so nothing was sent to the Mac.`
142
+ );
143
+ }
144
+ if (requestedApp !== void 0 && !sameApp(grant.target_app, requestedApp)) {
145
+ return refuse(
146
+ `computer-use: your session is bound to '${grant.target_app ?? "(unknown)"}' and this call targets '${requestedApp ?? "(no app named)"}'. Nothing was sent to the Mac. A grant covers ONE application - if you genuinely need the other one, request a separate session for it and say why.`
147
+ );
148
+ }
149
+ const sessionRequestId = grant.request_id ?? "";
150
+ try {
151
+ const filed = await deps.apiPost(ACTION_PATH, {
152
+ agent_id: deps.agentId,
153
+ session_request_id: sessionRequestId,
154
+ tool: toolName,
155
+ target_app: grant.target_app,
156
+ target_element: previewText(readElement(args), 80),
157
+ text_preview: previewText(readTypedText(args)),
158
+ returns_screenshot: ctx?.includeScreenshot === true,
159
+ floor,
160
+ idempotency_key: actionIdempotencyKey(sessionRequestId, toolName, args),
161
+ summary: describeAction(toolName, grant.target_app, args)
162
+ });
163
+ switch (filed.status) {
164
+ case "auto_approve":
165
+ case "active":
166
+ return null;
167
+ // forward to the Mac
168
+ case "pending":
169
+ return refuse(
170
+ `computer-use: this action needs a human tap first (request ${filed.request_id}). Nothing was sent to the Mac. Poll with check_approval and re-issue this EXACT call once it is approved - re-issuing it re-attaches to the same request rather than filing another.`
171
+ );
172
+ case "denied":
173
+ case "hard_deny":
174
+ return refuse(
175
+ `computer-use: a human DENIED this action` + (filed.denial_reason ? `: ${filed.denial_reason}` : ".") + ` Nothing was sent to the Mac. Do not retry it - pick a different approach or ask them what to do.`
176
+ );
177
+ case "expired":
178
+ return refuse(
179
+ `computer-use: the approval for this action expired before anyone answered (request ${filed.request_id}). Nothing was sent to the Mac. This is not a refusal - re-issue the call to ask again.`
180
+ );
181
+ case "revoked":
182
+ return refuse(
183
+ `computer-use: your session was revoked while this action was awaiting approval. Nothing was sent to the Mac.`
184
+ );
185
+ case "consumed":
186
+ return refuse(
187
+ `computer-use: this exact action was already approved and already performed once, so it was NOT sent again. An approval covers ONE execution - if you genuinely need to repeat it, that is a new request. (If you are retrying because you could not tell whether the first one landed: read the accessibility tree, isError:false is not evidence either way.)`
188
+ );
189
+ default:
190
+ return refuse(
191
+ `computer-use: the control plane returned an approval status this proxy does not recognise ('${String(filed.status)}'), so the call was NOT sent to the Mac. Tell your operator.`
192
+ );
193
+ }
194
+ } catch (err) {
195
+ logErr2(`action filing failed: ${err.message}`);
196
+ return refuse(
197
+ `computer-use: the approval for this action could not be filed (${err.message}), so it was NOT sent to the Mac. This is not a denial. Retry shortly; if it persists, tell your operator.`
198
+ );
199
+ }
200
+ };
201
+ }
202
+ function readElement(args) {
203
+ if (typeof args !== "object" || args === null) return void 0;
204
+ const rec = args;
205
+ for (const key of ["element_index", "element", "role", "title"]) {
206
+ const v = rec[key];
207
+ if (typeof v === "string" && v.trim()) return v;
208
+ if (typeof v === "number") return String(v);
209
+ }
210
+ return void 0;
211
+ }
212
+ function readTypedText(args) {
213
+ if (typeof args !== "object" || args === null) return void 0;
214
+ const rec = args;
215
+ for (const key of ["text", "value", "keys", "key"]) {
216
+ const v = rec[key];
217
+ if (typeof v === "string" && v.length > 0) return v;
218
+ }
219
+ return void 0;
220
+ }
221
+ function describeAction(tool, app, args) {
222
+ const where = app ? ` in ${app}` : "";
223
+ const el = readElement(args);
224
+ const elPart = el !== void 0 ? ` on element ${String(el)}` : "";
225
+ const text = previewText(readTypedText(args), 80);
226
+ const textPart = text ? ` with "${text}"` : "";
227
+ return `${tool}${elPart}${textPart}${where}`;
228
+ }
229
+ function createUnconfiguredGate(missing) {
230
+ return async () => refuse(
231
+ `computer-use: this proxy is not configured to check approvals (missing ${missing.join(", ")}), so it refuses every call rather than forwarding un-approved ones to the Mac. Nothing has been sent. Ask your operator to re-provision this agent.`
232
+ );
233
+ }
234
+ function resolveGate(env, makeClient) {
235
+ const agtHost = env.AGT_HOST?.trim();
236
+ const agtApiKey = env.AGT_API_KEY?.trim();
237
+ const agentId = env.AGT_AGENT_ID?.trim();
238
+ const missing = [];
239
+ if (!agtHost) missing.push("AGT_HOST");
240
+ if (!agtApiKey) missing.push("AGT_API_KEY");
241
+ if (!agentId) missing.push("AGT_AGENT_ID");
242
+ if (missing.length > 0) return { gate: createUnconfiguredGate(missing), missing };
243
+ const client = makeClient({ agtHost, agtApiKey, agentId });
244
+ return {
245
+ gate: createApprovalGate({ agentId, apiGet: client.apiGet, apiPost: client.apiPost }),
246
+ missing: []
247
+ };
248
+ }
249
+
250
+ // src/computer-use-api-client.ts
251
+ var REQUEST_TIMEOUT_MS = 1e4;
252
+ var TOKEN_SKEW_MS = 6e4;
253
+ function createHostApiClient(config) {
254
+ const fetchImpl = config.fetchImpl ?? fetch;
255
+ const now = config.now ?? Date.now;
256
+ const base = config.agtHost.replace(/\/+$/, "");
257
+ let cachedToken = null;
258
+ let cachedTokenExpiresAt = 0;
259
+ async function getToken() {
260
+ if (cachedToken && now() < cachedTokenExpiresAt) return cachedToken;
261
+ const res = await fetchImpl(`${base}/host/exchange`, {
262
+ method: "POST",
263
+ headers: { "Content-Type": "application/json" },
264
+ body: JSON.stringify({ host_key: config.agtApiKey, agent_id: config.agentId }),
265
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
266
+ });
267
+ if (!res.ok) {
268
+ const text = await res.text().catch(() => res.statusText);
269
+ throw new Error(`host key exchange returned ${res.status}: ${text}`);
270
+ }
271
+ const body = await res.json();
272
+ if (!body.token) throw new Error("host key exchange returned no token");
273
+ cachedToken = body.token;
274
+ const lifetimeMs = (body.expires_in ?? 3600) * 1e3;
275
+ cachedTokenExpiresAt = now() + Math.max(0, lifetimeMs - TOKEN_SKEW_MS);
276
+ return cachedToken;
277
+ }
278
+ async function request(path, init, retried = false) {
279
+ const token = await getToken();
280
+ const res = await fetchImpl(`${base}${path}`, {
281
+ ...init,
282
+ headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` },
283
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
284
+ });
285
+ if (res.status === 401 && !retried) {
286
+ cachedToken = null;
287
+ cachedTokenExpiresAt = 0;
288
+ return request(path, init, true);
289
+ }
290
+ if (!res.ok) {
291
+ const text = await res.text().catch(() => res.statusText);
292
+ throw new Error(`API ${path} returned ${res.status}: ${text}`);
293
+ }
294
+ return await res.json();
295
+ }
296
+ return {
297
+ apiGet: (path) => request(path, { method: "GET" }),
298
+ apiPost: (path, body) => request(path, {
299
+ method: "POST",
300
+ headers: { "Content-Type": "application/json" },
301
+ body: JSON.stringify(body)
302
+ })
303
+ };
304
+ }
305
+
306
+ // src/computer-use-proxy.ts
10
307
  var INCLUDE_SCREENSHOT_PARAM = "include_screenshot";
11
308
  var STRIPPED_IMAGE_STUB = `[screenshot omitted to save tokens \u2014 pass ${INCLUDE_SCREENSHOT_PARAM}: true to receive it. Element indices in the accessibility tree above are usually enough to act.]`;
12
309
  var RELIABILITY_NOTE = `
@@ -108,6 +405,7 @@ function logErr(msg) {
108
405
  var allowAll = async () => null;
109
406
  function createProxy(deps) {
110
407
  const beforeForward = deps.beforeForward ?? allowAll;
408
+ const allowUnrequestedScreenshot = deps.allowUnrequestedScreenshot !== false;
111
409
  const pending = /* @__PURE__ */ new Map();
112
410
  async function handleDownstream(line) {
113
411
  const text = line.trim();
@@ -126,7 +424,7 @@ function createProxy(deps) {
126
424
  const { include, params } = takeIncludeScreenshot(frame.params);
127
425
  const toolName = typeof params?.name === "string" ? params.name : "";
128
426
  const args = params?.arguments;
129
- const short = await beforeForward(toolName, args);
427
+ const short = await beforeForward(toolName, args, { includeScreenshot: include });
130
428
  if (short) {
131
429
  if (!isRequest) {
132
430
  logErr(`short-circuited notification tools/call ${toolName} (no reply sent)`);
@@ -171,8 +469,11 @@ function createProxy(deps) {
171
469
  (b) => typeof b === "object" && b !== null && b.type === "text"
172
470
  );
173
471
  if (treeBlock && !treeHasActionableContent(treeBlock.text)) {
174
- logErr("tree carries no actionable content; keeping screenshot");
175
- return [text];
472
+ if (allowUnrequestedScreenshot) {
473
+ logErr("tree carries no actionable content; keeping screenshot");
474
+ return [text];
475
+ }
476
+ logErr("tree carries no actionable content, but screenshots are gated; stripping");
176
477
  }
177
478
  }
178
479
  const { result, stripped } = stripImages(frame.result);
@@ -249,11 +550,23 @@ async function main(argv = process.argv) {
249
550
  logErr(`failed to spawn upstream: ${err.message}`);
250
551
  process.exit(1);
251
552
  }
553
+ const { gate, missing } = resolveGate(process.env, (c) => createHostApiClient(c));
554
+ if (missing.length > 0) {
555
+ logErr(
556
+ `NOT CONFIGURED for approval checks (missing ${missing.join(", ")}); refusing every call. No computer-use tool will reach the Mac until this agent is re-provisioned.`
557
+ );
558
+ }
252
559
  await wirePump({
253
560
  child,
254
561
  clientIn: process.stdin,
255
562
  clientOut: process.stdout,
256
- proxy: createProxy({ upstream })
563
+ proxy: createProxy({
564
+ upstream,
565
+ beforeForward: gate,
566
+ // With a gate in place an image must never arrive unrequested - see the
567
+ // field's own note on why the accessibility-blind hatch closes here.
568
+ allowUnrequestedScreenshot: false
569
+ })
257
570
  });
258
571
  }
259
572
  var invokedDirectly = (() => {
@@ -37310,10 +37310,22 @@ var FLAG_REGISTRY = [
37310
37310
  // See the description: `shadow` is what prod compiles today, so it is the
37311
37311
  // only default that ships this flag as a no-op.
37312
37312
  defaultValue: "shadow",
37313
- // enforce changes the admission rule on the platform's front door, and the
37314
- // families it starts refusing include mcp-proxy one of the fleet's
37315
- // dominant callers. This is precisely the deliberate, audited flip ADR-0022
37316
- // §4 reserves confirmation for.
37313
+ // enforce changes the admission rule on the platform's front door. This is
37314
+ // precisely the deliberate, audited flip ADR-0022 §4 reserves confirmation
37315
+ // for, and it stays `sensitive` — but NOT for the reason first written here.
37316
+ // That read "the families it starts refusing include mcp-proxy, one of the
37317
+ // fleet's dominant callers", and ENG-9895 measured otherwise: mcp-proxy
37318
+ // verifies its own token on a route carrying no `authMiddleware`, so it is a
37319
+ // dominant caller of a different door and cannot be in the refused set. The
37320
+ // measured population at that door is ZERO over 298,839,353 prod log records
37321
+ // / 14 days, and the impersonation families are absent too — one
37322
+ // structurally (`X-Agent-Impersonation` header, own verifier), one by
37323
+ // measurement (307 requests through that door, zero lines). What keeps this
37324
+ // `sensitive` is therefore NOT an unresolved residue: it is that `enforce`
37325
+ // changes an admission rule on the front door, and that the predicate is
37326
+ // structural, so a FUTURE family 13 is born refusable without an edit here.
37327
+ // That is exactly the deliberate, audited flip ADR-0022 §4 reserves
37328
+ // confirmation for. docs/research/eng-9895-prod-shadow-measurement.md
37317
37329
  sensitive: true
37318
37330
  },
37319
37331
  {
package/dist/mcp/index.js CHANGED
@@ -29580,10 +29580,22 @@ var FLAG_REGISTRY = [
29580
29580
  // See the description: `shadow` is what prod compiles today, so it is the
29581
29581
  // only default that ships this flag as a no-op.
29582
29582
  defaultValue: "shadow",
29583
- // enforce changes the admission rule on the platform's front door, and the
29584
- // families it starts refusing include mcp-proxy one of the fleet's
29585
- // dominant callers. This is precisely the deliberate, audited flip ADR-0022
29586
- // §4 reserves confirmation for.
29583
+ // enforce changes the admission rule on the platform's front door. This is
29584
+ // precisely the deliberate, audited flip ADR-0022 §4 reserves confirmation
29585
+ // for, and it stays `sensitive` — but NOT for the reason first written here.
29586
+ // That read "the families it starts refusing include mcp-proxy, one of the
29587
+ // fleet's dominant callers", and ENG-9895 measured otherwise: mcp-proxy
29588
+ // verifies its own token on a route carrying no `authMiddleware`, so it is a
29589
+ // dominant caller of a different door and cannot be in the refused set. The
29590
+ // measured population at that door is ZERO over 298,839,353 prod log records
29591
+ // / 14 days, and the impersonation families are absent too — one
29592
+ // structurally (`X-Agent-Impersonation` header, own verifier), one by
29593
+ // measurement (307 requests through that door, zero lines). What keeps this
29594
+ // `sensitive` is therefore NOT an unresolved residue: it is that `enforce`
29595
+ // changes an admission rule on the front door, and that the predicate is
29596
+ // structural, so a FUTURE family 13 is born refusable without an edit here.
29597
+ // That is exactly the deliberate, audited flip ADR-0022 §4 reserves
29598
+ // confirmation for. docs/research/eng-9895-prod-shadow-measurement.md
29587
29599
  sensitive: true
29588
29600
  },
29589
29601
  {
@@ -44565,6 +44577,22 @@ function renderKanbanFullCard(item, nowMs = Date.now()) {
44565
44577
  }
44566
44578
  return lines.join("\n");
44567
44579
  }
44580
+ function renderKanbanNoteHistory(requested, history, truncated) {
44581
+ if (!requested) return "";
44582
+ const entries = history ?? [];
44583
+ if (entries.length === 0) {
44584
+ return "\n\n## Durable note history\nNo note events recorded for this card. Notes written before the durable mirror existed will not appear here.";
44585
+ }
44586
+ const ordered = entries;
44587
+ const head = `
44588
+
44589
+ ## Durable note history (${ordered.length}${truncated ? ", TRUNCATED \u2014 older entries exist beyond this page" : ""})
44590
+ The full text of every appended note, including entries that have rolled off the ~4 KB \`notes\` column.
44591
+ `;
44592
+ return head + ordered.map((e) => `
44593
+ [${e.at}]
44594
+ ${e.note}`).join("\n");
44595
+ }
44568
44596
 
44569
44597
  // src/kanban-peek-render.ts
44570
44598
  var PEEK_RENDER_COLUMNS = [
@@ -45802,12 +45830,22 @@ server.tool(
45802
45830
  "kanban_get",
45803
45831
  'Read ONE of your own kanban cards IN FULL, with no truncation anywhere - description, notes, result, deliverable, every field. kanban_list caps a description at 1,200 characters and kanban_search at 300; this is the one surface where "read the whole thing" always means the whole thing. Use it whenever a card matters enough to read completely - especially a scheduled-task card, whose description IS the routine prompt you are executing: a long one may already be silently truncated everywhere else you have seen it.',
45804
45832
  {
45805
- id: external_exports.string().describe("The card id (from kanban_list, kanban_search, or any other kanban tool's response).")
45833
+ id: external_exports.string().describe("The card id (from kanban_list, kanban_search, or any other kanban tool's response)."),
45834
+ include_note_history: external_exports.boolean().optional().describe(
45835
+ "CS-1683: also return the DURABLE note history. `notes` is capped at ~4 KB and rolls off oldest-first, but every appended breadcrumb is retained in full elsewhere. Use this when a note you remember writing is no longer in `notes` \u2014 that text is not gone, it has rolled off the inline column."
45836
+ )
45806
45837
  },
45807
- async ({ id }) => {
45838
+ async ({ id, include_note_history }) => {
45808
45839
  let data;
45809
45840
  try {
45810
- data = await apiPost("/host/kanban/get", { agent_id: AGT_AGENT_ID, id }, false, 15e3, "kanban_get", true);
45841
+ data = await apiPost(
45842
+ "/host/kanban/get",
45843
+ { agent_id: AGT_AGENT_ID, id, include_note_history: include_note_history === true },
45844
+ false,
45845
+ 15e3,
45846
+ "kanban_get",
45847
+ true
45848
+ );
45811
45849
  } catch (err) {
45812
45850
  const status = err instanceof ApiPostError ? err.status : null;
45813
45851
  return {
@@ -45817,7 +45855,19 @@ server.tool(
45817
45855
  if (!data.item) {
45818
45856
  return { content: [{ type: "text", text: kanbanGetErrorMessage(404, id, "card not found") }] };
45819
45857
  }
45820
- return { content: [{ type: "text", text: renderKanbanFullCard(data.item) }] };
45858
+ const card = renderKanbanFullCard(data.item);
45859
+ return {
45860
+ content: [
45861
+ {
45862
+ type: "text",
45863
+ text: card + renderKanbanNoteHistory(
45864
+ include_note_history === true,
45865
+ data.note_history,
45866
+ data.note_history_truncated
45867
+ )
45868
+ }
45869
+ ]
45870
+ };
45821
45871
  }
45822
45872
  );
45823
45873
  server.tool(
@@ -43533,10 +43533,22 @@ var FLAG_REGISTRY = [
43533
43533
  // See the description: `shadow` is what prod compiles today, so it is the
43534
43534
  // only default that ships this flag as a no-op.
43535
43535
  defaultValue: "shadow",
43536
- // enforce changes the admission rule on the platform's front door, and the
43537
- // families it starts refusing include mcp-proxy one of the fleet's
43538
- // dominant callers. This is precisely the deliberate, audited flip ADR-0022
43539
- // §4 reserves confirmation for.
43536
+ // enforce changes the admission rule on the platform's front door. This is
43537
+ // precisely the deliberate, audited flip ADR-0022 §4 reserves confirmation
43538
+ // for, and it stays `sensitive` — but NOT for the reason first written here.
43539
+ // That read "the families it starts refusing include mcp-proxy, one of the
43540
+ // fleet's dominant callers", and ENG-9895 measured otherwise: mcp-proxy
43541
+ // verifies its own token on a route carrying no `authMiddleware`, so it is a
43542
+ // dominant caller of a different door and cannot be in the refused set. The
43543
+ // measured population at that door is ZERO over 298,839,353 prod log records
43544
+ // / 14 days, and the impersonation families are absent too — one
43545
+ // structurally (`X-Agent-Impersonation` header, own verifier), one by
43546
+ // measurement (307 requests through that door, zero lines). What keeps this
43547
+ // `sensitive` is therefore NOT an unresolved residue: it is that `enforce`
43548
+ // changes an admission rule on the front door, and that the predicate is
43549
+ // structural, so a FUTURE family 13 is born refusable without an edit here.
43550
+ // That is exactly the deliberate, audited flip ADR-0022 §4 reserves
43551
+ // confirmation for. docs/research/eng-9895-prod-shadow-measurement.md
43540
43552
  sensitive: true
43541
43553
  },
43542
43554
  {