Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions lib/project-sessions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var fs = require("fs");
var path = require("path");
var utils = require("./utils");
var { execFileSync, execFile } = require("child_process");
var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
var liteDetect = require("./lite-detect");
Expand Down Expand Up @@ -368,7 +369,11 @@ function attachSessions(ctx) {
// If Clay already has a persisted meta file for this cliSessionId, read
// its vendor so resumeSession doesn't silently default to the project's
// primary vendor (which would break codex sessions after server restart).
// Also read allowedTools (lr-8b2e) so a previously-granted "allow for
// session" decision survives this rehydration path instead of
// re-prompting — resumeSession() itself defaults to {} when absent.
var persistedVendor = null;
var persistedAllowedTools = null;
try {
var _fsResume = require("fs");
var _pathResume = require("path");
Expand All @@ -378,6 +383,12 @@ function attachSessions(ctx) {
try {
var metaObj = JSON.parse(firstLine);
if (metaObj && metaObj.type === "meta" && metaObj.vendor) persistedVendor = metaObj.vendor;
// Sanitize before it ever reaches resumeSession() (lr-8b2e
// hardening) — a crafted meta record must not be able to
// auto-approve a tool with no operator click.
if (metaObj && metaObj.type === "meta" && metaObj.allowedTools) {
persistedAllowedTools = utils.sanitizeAllowedTools(metaObj.allowedTools);
}
} catch (e) {}
}
} catch (e) {}
Expand All @@ -402,10 +413,10 @@ function attachSessions(ctx) {
}
}
}
var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title, vendor: persistedVendor || undefined }, ws);
var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title, vendor: persistedVendor || undefined, allowedTools: persistedAllowedTools || undefined }, ws);
if (resumed) ws._clayActiveSession = resumed.localId;
}).catch(function() {
var resumed = sm.resumeSession(msg.cliSessionId, persistedVendor ? { vendor: persistedVendor } : undefined, ws);
var resumed = sm.resumeSession(msg.cliSessionId, (persistedVendor || persistedAllowedTools) ? { vendor: persistedVendor, allowedTools: persistedAllowedTools } : undefined, ws);
if (resumed) ws._clayActiveSession = resumed.localId;
});
return true;
Expand Down Expand Up @@ -1241,6 +1252,11 @@ function attachSessions(ctx) {
if (decision === "allow_always") {
if (!session.allowedTools) session.allowedTools = {};
session.allowedTools[pending.toolName] = true;
// Flush immediately (lr-8b2e) so the grant survives daemon restart /
// resume-by-cliSessionId rehydration even without a later save
// trigger — previously this lived only in the in-memory session
// object and was lost on rebuild, causing a spurious re-prompt.
sm.saveSessionFile(session);
}
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
} else {
Expand Down
19 changes: 18 additions & 1 deletion lib/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ function createSessionManager(opts) {
if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
if (session.agentName) metaObj.agentName = session.agentName;
if (session.loop) metaObj.loop = session.loop;
// Persist per-session "allow for session" tool grants (lr-8b2e) so they
// survive daemon restart / resume-by-cliSessionId rehydration instead of
// silently re-prompting. Only written when non-empty — grants that were
// never given must stay absent, not round-trip as {}.
if (session.allowedTools && Object.keys(session.allowedTools).length > 0) {
metaObj.allowedTools = session.allowedTools;
}
var meta = JSON.stringify(metaObj);
var lines = [meta];
for (var i = 0; i < session.history.length; i++) {
Expand Down Expand Up @@ -285,6 +292,12 @@ function createSessionManager(opts) {
sentToolResults: {},
pendingPermissions: {},
pendingAskUser: {},
// Hydrate previously-granted "allow for session" tool decisions from
// durable state (lr-8b2e); default to {} only when none was saved so
// grants that were never given remain empty, not re-prompted.
// Sanitized (lr-8b2e hardening) so a malformed/injected persisted
// record cannot silently auto-approve a tool.
allowedTools: utils.sanitizeAllowedTools(m.allowedTools),
isProcessing: false,
title: m.title || "",
createdAt: m.createdAt || Date.now(),
Expand Down Expand Up @@ -826,7 +839,11 @@ function createSessionManager(opts) {
sentToolResults: {},
pendingPermissions: {},
pendingAskUser: {},
allowedTools: {},
// Hydrate previously-granted "allow for session" tool decisions from
// durable state (lr-8b2e); default to {} only when none was saved.
// Sanitized (lr-8b2e hardening) so a malformed/injected persisted
// record cannot silently auto-approve a tool.
allowedTools: utils.sanitizeAllowedTools(opts && opts.allowedTools),
isProcessing: false,
title: title,
createdAt: Date.now(),
Expand Down
23 changes: 23 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,31 @@ function resolveEncodedFile(baseDir, cwd, ext) {
});
}

/**
* Sanitize a persisted `allowedTools` map before hydrating it into a live
* session (lr-8b2e). The value originates from a durable .jsonl meta record
* that shares its trust boundary with other session-file fields, but an
* un-validated shape would let a crafted record (e.g. {"Bash": "yes"} or a
* non-object) auto-approve a tool with no operator click at the
* handleCanUseTool check (lib/sdk-bridge.js:691).
*
* Keeps only entries whose key is a string and whose value is strictly
* boolean `true`; drops everything else. A non-object input (including
* null/undefined/arrays) yields {} rather than throwing.
*/
function sanitizeAllowedTools(value) {
var out = {};
if (!value || typeof value !== "object" || Array.isArray(value)) return out;
for (var key in value) {
if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
if (typeof key === "string" && value[key] === true) out[key] = true;
}
return out;
}

module.exports = {
encodeCwd: encodeCwd,
resolveEncodedDir: resolveEncodedDir,
resolveEncodedFile: resolveEncodedFile,
sanitizeAllowedTools: sanitizeAllowedTools,
};
Loading
Loading