codex-agent-view 0.4.6 → 0.4.8

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.
@@ -232,6 +232,27 @@ function addActivity(session, event, status, limit) {
232
232
  }
233
233
  }
234
234
 
235
+ function refineUnresolvedStartActivity(
236
+ session,
237
+ { type, idField, id, status },
238
+ ) {
239
+ const unresolvedStatuses = new Set([
240
+ "running",
241
+ "completion_not_observed",
242
+ "interrupted",
243
+ ]);
244
+ for (const activity of session.recent_activities) {
245
+ if (
246
+ activity.type === type &&
247
+ activity[idField] === id &&
248
+ unresolvedStatuses.has(activity.status)
249
+ ) {
250
+ activity.status = status;
251
+ break;
252
+ }
253
+ }
254
+ }
255
+
235
256
  function applySessionEvent(session, event, limits) {
236
257
  const lifecycle = session.lifecycle;
237
258
  if (event.type === "session_started") {
@@ -377,6 +398,14 @@ function applySubagentEvent(session, event, limits) {
377
398
  agent.stopped_at_ms = event.received_at_ms;
378
399
  agent.status = agent.start_observed ? "stopped" : "stopped_without_start";
379
400
  agent.has_out_of_order_events = !agent.start_observed;
401
+ if (agent.start_observed) {
402
+ refineUnresolvedStartActivity(session, {
403
+ type: "subagent_started",
404
+ idField: "agent_id",
405
+ id: event.agent_id,
406
+ status: "stopped",
407
+ });
408
+ }
380
409
  }
381
410
 
382
411
  agent.agent_type = event.agent_type;
@@ -430,6 +459,14 @@ function applyToolEvent(session, event, limits) {
430
459
  tool.status = tool.start_observed ? "completed" : "completed_without_start";
431
460
  tool.has_out_of_order_events = !tool.start_observed;
432
461
  activityStatus = tool.status;
462
+ if (tool.start_observed) {
463
+ refineUnresolvedStartActivity(session, {
464
+ type: "tool_started",
465
+ idField: "tool_use_id",
466
+ id: event.tool_use_id,
467
+ status: "completed",
468
+ });
469
+ }
433
470
 
434
471
  const permission = session.permission;
435
472
  if (
@@ -1,6 +1,6 @@
1
1
  import { createServer } from "node:http";
2
2
  import { readFile } from "node:fs/promises";
3
- import { timingSafeEqual } from "node:crypto";
3
+ import { createHmac, timingSafeEqual } from "node:crypto";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  import { createMonitorStore } from "../core/index.mjs";
@@ -29,6 +29,20 @@ const SECURITY_HEADERS = {
29
29
  "x-frame-options": "DENY",
30
30
  };
31
31
 
32
+ const RECOVERY_HEADER = "x-codex-agent-view-recovery";
33
+ const ACCESS_HEADER = "x-codex-agent-view-access";
34
+ const BOOTSTRAP_SCOPE = "viewer_bootstrap";
35
+ const RECOVERY_SCOPE = "viewer_recovery";
36
+ const ACCESS_SCOPE = "viewer_access";
37
+ const BOOTSTRAP_TTL_MS = 60 * 1_000;
38
+ const RECOVERY_TTL_MS = 30 * 60 * 1_000;
39
+ const ACCESS_TTL_MS = 15 * 60 * 1_000;
40
+ const MAX_SIGNED_CREDENTIAL_LENGTH = 1_024;
41
+ const MAX_USED_BOOTSTRAP_GRANTS = 256;
42
+ const OWNERSHIP_PROOF_DOMAIN = "codex-agent-view/runtime-ownership/v1";
43
+ const OWNERSHIP_NONCE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
44
+ const CANONICAL_SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
45
+
32
46
  function sendJson(response, statusCode, value, extraHeaders = {}) {
33
47
  response.writeHead(statusCode, {
34
48
  ...SECURITY_HEADERS,
@@ -38,14 +52,12 @@ function sendJson(response, statusCode, value, extraHeaders = {}) {
38
52
  response.end(`${JSON.stringify(value)}\n`);
39
53
  }
40
54
 
41
- function isLoopbackHostHeader(value) {
42
- if (typeof value !== "string") {
43
- return false;
44
- }
45
- const hostname = value.startsWith("[")
46
- ? value.slice(1, value.indexOf("]"))
47
- : value.split(":", 1)[0];
48
- return hostname === LOOPBACK_HOST || hostname === "localhost" || hostname === "::1";
55
+ function isOriginFormRequestTarget(value) {
56
+ return (
57
+ typeof value === "string" &&
58
+ /^\/(?!\/)/.test(value) &&
59
+ !value.includes("\\")
60
+ );
49
61
  }
50
62
 
51
63
  function hasToken(request, token) {
@@ -58,6 +70,120 @@ function hasToken(request, token) {
58
70
  return supplied.length === expected.length && timingSafeEqual(supplied, expected);
59
71
  }
60
72
 
73
+ function signCredential(payload, signingToken) {
74
+ const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url");
75
+ const signature = createHmac("sha256", signingToken)
76
+ .update(encodedPayload)
77
+ .digest("base64url");
78
+ return `${encodedPayload}.${signature}`;
79
+ }
80
+
81
+ function createSignedCredential({
82
+ audience,
83
+ excludeSessionId = null,
84
+ grantId,
85
+ familyExpiresAtMs,
86
+ nowMs,
87
+ scope,
88
+ ttlMs,
89
+ signingToken,
90
+ }) {
91
+ const expiresAtMs = Math.min(nowMs + ttlMs, familyExpiresAtMs);
92
+ const payload = {
93
+ aud: audience,
94
+ exclude_session_id: excludeSessionId,
95
+ exp: expiresAtMs,
96
+ family_exp: familyExpiresAtMs,
97
+ scope,
98
+ v: 1,
99
+ };
100
+ if (scope === BOOTSTRAP_SCOPE) {
101
+ payload.jti = grantId;
102
+ }
103
+ return {
104
+ credential: signCredential(payload, signingToken),
105
+ expiresAtMs,
106
+ };
107
+ }
108
+
109
+ function validateSignedCredential(
110
+ credential,
111
+ { audience, nowMs, scope, ttlMs, signingToken },
112
+ ) {
113
+ if (
114
+ typeof credential !== "string" ||
115
+ credential.length === 0 ||
116
+ credential.length > MAX_SIGNED_CREDENTIAL_LENGTH
117
+ ) {
118
+ return null;
119
+ }
120
+ const parts = credential.split(".");
121
+ if (parts.length !== 2 || !parts.every((part) => /^[A-Za-z0-9_-]+$/.test(part))) {
122
+ return null;
123
+ }
124
+ const [encodedPayload, suppliedSignature] = parts;
125
+ const expectedSignature = createHmac("sha256", signingToken)
126
+ .update(encodedPayload)
127
+ .digest("base64url");
128
+ const supplied = Buffer.from(suppliedSignature);
129
+ const expected = Buffer.from(expectedSignature);
130
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) {
131
+ return null;
132
+ }
133
+
134
+ let payload;
135
+ try {
136
+ payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
137
+ } catch {
138
+ return null;
139
+ }
140
+ const expectedKeys = scope === BOOTSTRAP_SCOPE
141
+ ? "aud,exclude_session_id,exp,family_exp,jti,scope,v"
142
+ : "aud,exclude_session_id,exp,family_exp,scope,v";
143
+ if (
144
+ payload === null ||
145
+ typeof payload !== "object" ||
146
+ Array.isArray(payload) ||
147
+ Object.keys(payload).sort().join(",") !== expectedKeys ||
148
+ payload.v !== 1 ||
149
+ payload.scope !== scope ||
150
+ payload.aud !== audience ||
151
+ !(
152
+ payload.exclude_session_id === null ||
153
+ (
154
+ typeof payload.exclude_session_id === "string" &&
155
+ CANONICAL_SESSION_ID_PATTERN.test(payload.exclude_session_id)
156
+ )
157
+ ) ||
158
+ (scope === BOOTSTRAP_SCOPE && !/^[A-Za-z0-9_-]{43}$/.test(payload.jti)) ||
159
+ !Number.isSafeInteger(payload.exp) ||
160
+ !Number.isSafeInteger(payload.family_exp) ||
161
+ payload.exp <= nowMs ||
162
+ payload.exp > nowMs + ttlMs ||
163
+ payload.family_exp <= nowMs ||
164
+ payload.exp > payload.family_exp
165
+ ) {
166
+ return null;
167
+ }
168
+ return payload;
169
+ }
170
+
171
+ function createOwnershipProof(nonce, runtimeToken) {
172
+ return createHmac("sha256", runtimeToken)
173
+ .update(OWNERSHIP_PROOF_DOMAIN)
174
+ .update("\0")
175
+ .update(nonce)
176
+ .digest("base64url");
177
+ }
178
+
179
+ function bearerValue(request) {
180
+ const authorization = request.headers.authorization;
181
+ if (typeof authorization !== "string" || !authorization.startsWith("Bearer ")) {
182
+ return "";
183
+ }
184
+ return authorization.slice("Bearer ".length);
185
+ }
186
+
61
187
  async function readJsonBody(request) {
62
188
  const chunks = [];
63
189
  let bytes = 0;
@@ -97,28 +223,259 @@ export async function startMonitorServer({
97
223
  throw new Error(`monitor server must bind to ${LOOPBACK_HOST}`);
98
224
  }
99
225
 
226
+ const usedBootstrapGrants = new Map();
100
227
  const server = createServer(async (request, response) => {
101
228
  try {
102
- if (!isLoopbackHostHeader(request.headers.host)) {
103
- sendJson(response, 421, { error: "loopback host required" });
229
+ const address = server.address();
230
+ if (address === null || typeof address === "string") {
231
+ sendJson(response, 503, { error: "monitor unavailable" });
232
+ return;
233
+ }
234
+ const audience = `http://${LOOPBACK_HOST}:${address.port}`;
235
+ const exactHost = `${LOOPBACK_HOST}:${address.port}`;
236
+ if (
237
+ request.headers.host !== exactHost ||
238
+ !isOriginFormRequestTarget(request.url)
239
+ ) {
240
+ sendJson(response, 421, { error: "exact monitor authority required" });
104
241
  return;
105
242
  }
106
243
 
107
- const requestUrl = new URL(request.url || "/", `http://${LOOPBACK_HOST}`);
244
+ const requestUrl = new URL(request.url, audience);
245
+ const nowMs = now();
108
246
  if (request.method === "GET" && requestUrl.pathname === "/api/health") {
109
247
  sendJson(response, 200, { ok: true });
110
248
  return;
111
249
  }
112
250
 
251
+ if (
252
+ request.method === "POST" &&
253
+ requestUrl.pathname === "/api/internal/ownership-proof"
254
+ ) {
255
+ if (request.headers["content-type"] !== "application/json") {
256
+ sendJson(response, 415, { error: "application/json required" });
257
+ return;
258
+ }
259
+ const payload = await readJsonBody(request);
260
+ if (
261
+ payload === null ||
262
+ typeof payload !== "object" ||
263
+ Array.isArray(payload) ||
264
+ Object.keys(payload).join(",") !== "nonce" ||
265
+ !OWNERSHIP_NONCE_PATTERN.test(payload.nonce)
266
+ ) {
267
+ sendJson(response, 400, { error: "invalid ownership challenge" });
268
+ return;
269
+ }
270
+ sendJson(response, 200, {
271
+ proof: createOwnershipProof(payload.nonce, token),
272
+ status: "owned",
273
+ });
274
+ return;
275
+ }
276
+
277
+ if (
278
+ request.method === "POST" &&
279
+ requestUrl.pathname === "/api/internal/viewer-grant"
280
+ ) {
281
+ if (!hasToken(request, token)) {
282
+ sendJson(response, 401, { error: "authorization required" });
283
+ return;
284
+ }
285
+ if (request.headers["content-type"] !== "application/json") {
286
+ sendJson(response, 415, { error: "application/json required" });
287
+ return;
288
+ }
289
+ const payload = await readJsonBody(request);
290
+ if (
291
+ payload === null ||
292
+ typeof payload !== "object" ||
293
+ Array.isArray(payload) ||
294
+ Object.keys(payload).join(",") !== "exclude_session_id" ||
295
+ !(
296
+ payload.exclude_session_id === null ||
297
+ (
298
+ typeof payload.exclude_session_id === "string" &&
299
+ CANONICAL_SESSION_ID_PATTERN.test(payload.exclude_session_id)
300
+ )
301
+ )
302
+ ) {
303
+ sendJson(response, 400, { error: "invalid viewer grant request" });
304
+ return;
305
+ }
306
+ const familyExpiresAtMs = nowMs + RECOVERY_TTL_MS;
307
+ const bootstrap = createSignedCredential({
308
+ audience,
309
+ excludeSessionId: payload.exclude_session_id,
310
+ familyExpiresAtMs,
311
+ grantId: createRuntimeToken(),
312
+ nowMs,
313
+ scope: BOOTSTRAP_SCOPE,
314
+ ttlMs: BOOTSTRAP_TTL_MS,
315
+ signingToken: token,
316
+ });
317
+ sendJson(response, 201, {
318
+ bootstrap_credential: bootstrap.credential,
319
+ expires_in_ms: BOOTSTRAP_TTL_MS,
320
+ status: "granted",
321
+ });
322
+ return;
323
+ }
324
+
113
325
  if (
114
326
  request.method === "GET" &&
115
327
  requestUrl.pathname === "/api/state"
116
328
  ) {
117
- if (!hasToken(request, token) && !hasToken(request, viewerToken)) {
329
+ const suppliedBearer = bearerValue(request);
330
+ const accessPayload = validateSignedCredential(suppliedBearer, {
331
+ audience,
332
+ nowMs,
333
+ scope: ACCESS_SCOPE,
334
+ ttlMs: ACCESS_TTL_MS,
335
+ signingToken: viewerToken,
336
+ });
337
+ const runtimeAuthorized = hasToken(request, token);
338
+ const rootViewerAuthorized = hasToken(request, viewerToken);
339
+ if (
340
+ !runtimeAuthorized &&
341
+ !rootViewerAuthorized &&
342
+ !accessPayload
343
+ ) {
118
344
  sendJson(response, 401, { error: "authorization required" });
119
345
  return;
120
346
  }
121
- sendJson(response, 200, store.getSnapshot());
347
+ const requestedExclusion = request.headers["x-codex-agent-view-exclude-session"];
348
+ const excludeSessionId = accessPayload?.exclude_session_id ?? (
349
+ rootViewerAuthorized &&
350
+ typeof requestedExclusion === "string" &&
351
+ CANONICAL_SESSION_ID_PATTERN.test(requestedExclusion)
352
+ ? requestedExclusion
353
+ : null
354
+ );
355
+ const extraHeaders = {};
356
+ if (rootViewerAuthorized || accessPayload) {
357
+ const familyExpiresAtMs = accessPayload?.family_exp ?? (
358
+ nowMs + RECOVERY_TTL_MS
359
+ );
360
+ const access = createSignedCredential({
361
+ audience,
362
+ excludeSessionId,
363
+ familyExpiresAtMs,
364
+ nowMs,
365
+ scope: ACCESS_SCOPE,
366
+ ttlMs: ACCESS_TTL_MS,
367
+ signingToken: viewerToken,
368
+ });
369
+ extraHeaders[ACCESS_HEADER] = access.credential;
370
+ }
371
+ if (rootViewerAuthorized) {
372
+ const familyExpiresAtMs = nowMs + RECOVERY_TTL_MS;
373
+ const recovery = createSignedCredential({
374
+ audience,
375
+ excludeSessionId,
376
+ familyExpiresAtMs,
377
+ nowMs,
378
+ scope: RECOVERY_SCOPE,
379
+ ttlMs: RECOVERY_TTL_MS,
380
+ signingToken: viewerToken,
381
+ });
382
+ extraHeaders[RECOVERY_HEADER] = recovery.credential;
383
+ }
384
+ sendJson(response, 200, store.getSnapshot(), {
385
+ ...extraHeaders,
386
+ });
387
+ return;
388
+ }
389
+
390
+ if (
391
+ request.method === "POST" &&
392
+ requestUrl.pathname === "/api/viewer/exchange"
393
+ ) {
394
+ if (
395
+ request.headers.origin !== audience ||
396
+ (
397
+ request.headers["sec-fetch-site"] !== undefined &&
398
+ request.headers["sec-fetch-site"] !== "same-origin"
399
+ )
400
+ ) {
401
+ sendJson(response, 403, { error: "same-origin request required" });
402
+ return;
403
+ }
404
+ if (request.headers["content-type"] !== "application/json") {
405
+ sendJson(response, 415, { error: "application/json required" });
406
+ return;
407
+ }
408
+ const payload = await readJsonBody(request);
409
+ const exactPayload =
410
+ payload === null ||
411
+ typeof payload !== "object" ||
412
+ Array.isArray(payload) ||
413
+ Object.keys(payload).join(",") !== "credential";
414
+ const bootstrapPayload = exactPayload ? null : validateSignedCredential(
415
+ payload.credential,
416
+ {
417
+ audience,
418
+ nowMs,
419
+ scope: BOOTSTRAP_SCOPE,
420
+ ttlMs: BOOTSTRAP_TTL_MS,
421
+ signingToken: token,
422
+ },
423
+ );
424
+ const recoveryPayload = exactPayload || bootstrapPayload
425
+ ? null
426
+ : validateSignedCredential(payload.credential, {
427
+ audience,
428
+ nowMs,
429
+ scope: RECOVERY_SCOPE,
430
+ ttlMs: RECOVERY_TTL_MS,
431
+ signingToken: viewerToken,
432
+ });
433
+ const credentialPayload = bootstrapPayload || recoveryPayload;
434
+ if (!credentialPayload) {
435
+ sendJson(response, 401, { error: "viewer exchange authorization required" });
436
+ return;
437
+ }
438
+ if (bootstrapPayload) {
439
+ for (const [grantId, expiresAtMs] of usedBootstrapGrants) {
440
+ if (expiresAtMs <= nowMs) usedBootstrapGrants.delete(grantId);
441
+ }
442
+ if (usedBootstrapGrants.has(bootstrapPayload.jti)) {
443
+ sendJson(response, 409, { error: "viewer grant already used" });
444
+ return;
445
+ }
446
+ if (usedBootstrapGrants.size >= MAX_USED_BOOTSTRAP_GRANTS) {
447
+ sendJson(response, 503, { error: "viewer grant capacity reached" });
448
+ return;
449
+ }
450
+ usedBootstrapGrants.set(bootstrapPayload.jti, bootstrapPayload.exp);
451
+ }
452
+ const excludeSessionId = credentialPayload.exclude_session_id;
453
+ const access = createSignedCredential({
454
+ audience,
455
+ excludeSessionId,
456
+ familyExpiresAtMs: credentialPayload.family_exp,
457
+ nowMs,
458
+ scope: ACCESS_SCOPE,
459
+ ttlMs: ACCESS_TTL_MS,
460
+ signingToken: viewerToken,
461
+ });
462
+ const recovery = createSignedCredential({
463
+ audience,
464
+ excludeSessionId,
465
+ familyExpiresAtMs: credentialPayload.family_exp,
466
+ nowMs,
467
+ scope: RECOVERY_SCOPE,
468
+ ttlMs: RECOVERY_TTL_MS,
469
+ signingToken: viewerToken,
470
+ });
471
+ sendJson(response, 200, {
472
+ access_credential: access.credential,
473
+ access_expires_in_ms: access.expiresAtMs - nowMs,
474
+ excluded_session_id: excludeSessionId,
475
+ recovery_credential: recovery.credential,
476
+ recovery_expires_in_ms: recovery.expiresAtMs - nowMs,
477
+ status: "exchanged",
478
+ });
122
479
  return;
123
480
  }
124
481