Skip to content

Commit 427b582

Browse files
AyeAyeclaude
andcommitted
security: migrate fetch_trakt_history from run_host_script (2/11, #35)
Dedicated MCP tool + IPC handler with minimal env: TRAKT_CLIENT_ID, TRAKT_ACCESS_TOKEN (2 vars). No process.env spread. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fb30ab3 commit 427b582

4 files changed

Lines changed: 108 additions & 11 deletions

File tree

container/agent-runner/src/ipc-mcp-stdio.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,13 @@ server.tool(
694694
async () => runHostOperation('sync_tripit'),
695695
);
696696

697+
server.tool(
698+
'fetch_trakt_history',
699+
'Fetch Trakt.tv watch history (shows, movies, ratings) for recommendations.',
700+
{},
701+
async () => runHostOperation('fetch_trakt_history'),
702+
);
703+
697704
server.tool(
698705
'github_backup',
699706
'Commit and push the group backup repo to GitHub. Use for nightly backups or when important state changes. The host handles git credentials — the container just triggers it.',

src/ipc.ts

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -842,10 +842,16 @@ export async function processTaskIpc(
842842
const scriptPath = path.join(groupDir, 'scripts', 'sync-tripit.sh');
843843
if (!fs.existsSync(scriptPath)) {
844844
const errPath = path.join(
845-
DATA_DIR, 'ipc', sourceGroup, 'input',
845+
DATA_DIR,
846+
'ipc',
847+
sourceGroup,
848+
'input',
846849
`_script_result_${data.requestId}.json`,
847850
);
848-
fs.writeFileSync(errPath, JSON.stringify({ error: 'sync-tripit.sh not found' }));
851+
fs.writeFileSync(
852+
errPath,
853+
JSON.stringify({ error: 'sync-tripit.sh not found' }),
854+
);
849855
break;
850856
}
851857

@@ -862,22 +868,106 @@ export async function processTaskIpc(
862868
'GOOGLE_REFRESH_TOKEN',
863869
]);
864870
const syncEnv: Record<string, string> = {
871+
PATH: process.env.PATH || '/usr/bin:/bin',
872+
HOME: process.env.HOME || '/root',
873+
TZ: process.env.TZ || 'UTC',
874+
...Object.fromEntries(Object.entries(syncVars).filter(([, v]) => v)),
875+
};
876+
877+
const scriptContent = fs.readFileSync(scriptPath, 'utf-8');
878+
const patchedContent = scriptContent.replace(
879+
/\/workspace\/group/g,
880+
groupDir,
881+
);
882+
const tmpScript = path.join(groupDir, '.tmp_host_sync-tripit.sh');
883+
fs.writeFileSync(tmpScript, patchedContent);
884+
885+
execFile(
886+
'bash',
887+
[tmpScript],
888+
{
889+
cwd: groupDir,
890+
env: syncEnv,
891+
timeout: 120_000,
892+
maxBuffer: 1024 * 1024,
893+
},
894+
(error, stdout, stderr) => {
895+
const resultPath = path.join(
896+
DATA_DIR,
897+
'ipc',
898+
sourceGroup,
899+
'input',
900+
`_script_result_${data.requestId}.json`,
901+
);
902+
if (error) {
903+
logger.error(
904+
{ sourceGroup, error: error.message, stderr },
905+
'sync_tripit failed',
906+
);
907+
fs.writeFileSync(
908+
resultPath,
909+
JSON.stringify({
910+
error: error.message,
911+
stderr: stderr.slice(-500),
912+
}),
913+
);
914+
} else {
915+
logger.info(
916+
{ sourceGroup, stdoutLen: stdout.length },
917+
'sync_tripit completed',
918+
);
919+
fs.writeFileSync(
920+
resultPath,
921+
JSON.stringify({ stdout, stderr: stderr || undefined }),
922+
);
923+
}
924+
try {
925+
fs.unlinkSync(tmpScript);
926+
} catch {
927+
/* best effort */
928+
}
929+
},
930+
);
931+
}
932+
break;
933+
934+
case 'fetch_trakt_history':
935+
if (data.requestId) {
936+
const groupDir = path.resolve(process.cwd(), 'groups', sourceGroup);
937+
const scriptPath = path.join(groupDir, 'scripts', 'trakt-watch-history.py');
938+
if (!fs.existsSync(scriptPath)) {
939+
const errPath = path.join(
940+
DATA_DIR, 'ipc', sourceGroup, 'input',
941+
`_script_result_${data.requestId}.json`,
942+
);
943+
fs.writeFileSync(errPath, JSON.stringify({ error: 'trakt-watch-history.py not found' }));
944+
break;
945+
}
946+
947+
logger.info({ sourceGroup }, 'Running fetch_trakt_history');
948+
949+
const { readEnvFile: readTraktEnv } = await import('./env.js');
950+
const traktVars = readTraktEnv([
951+
'TRAKT_CLIENT_ID',
952+
'TRAKT_ACCESS_TOKEN',
953+
]);
954+
const traktEnv: Record<string, string> = {
865955
PATH: process.env.PATH || '/usr/bin:/bin',
866956
HOME: process.env.HOME || '/root',
867957
TZ: process.env.TZ || 'UTC',
868958
...Object.fromEntries(
869-
Object.entries(syncVars).filter(([, v]) => v),
959+
Object.entries(traktVars).filter(([, v]) => v),
870960
),
871961
};
872962

873963
const scriptContent = fs.readFileSync(scriptPath, 'utf-8');
874964
const patchedContent = scriptContent.replace(/\/workspace\/group/g, groupDir);
875-
const tmpScript = path.join(groupDir, '.tmp_host_sync-tripit.sh');
965+
const tmpScript = path.join(groupDir, '.tmp_host_trakt-watch-history.py');
876966
fs.writeFileSync(tmpScript, patchedContent);
877967

878-
execFile('bash', [tmpScript], {
968+
execFile('python3', [tmpScript], {
879969
cwd: groupDir,
880-
env: syncEnv,
970+
env: traktEnv,
881971
timeout: 120_000,
882972
maxBuffer: 1024 * 1024,
883973
}, (error, stdout, stderr) => {
@@ -886,10 +976,10 @@ export async function processTaskIpc(
886976
`_script_result_${data.requestId}.json`,
887977
);
888978
if (error) {
889-
logger.error({ sourceGroup, error: error.message, stderr }, 'sync_tripit failed');
979+
logger.error({ sourceGroup, error: error.message, stderr }, 'fetch_trakt_history failed');
890980
fs.writeFileSync(resultPath, JSON.stringify({ error: error.message, stderr: stderr.slice(-500) }));
891981
} else {
892-
logger.info({ sourceGroup, stdoutLen: stdout.length }, 'sync_tripit completed');
982+
logger.info({ sourceGroup, stdoutLen: stdout.length }, 'fetch_trakt_history completed');
893983
fs.writeFileSync(resultPath, JSON.stringify({ stdout, stderr: stderr || undefined }));
894984
}
895985
try { fs.unlinkSync(tmpScript); } catch { /* best effort */ }

tiles/nanoclaw-admin/skills/nightly-housekeeping/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Rebuilds `travel-schedule.json` from the TripIt ICS feed. Silent on success; rep
4444
Report gaps; skip if all snoozed or complete.
4545

4646
## Step 5: Refresh Trakt watch history
47-
Use `mcp__nanoclaw__run_host_script(script: "trakt-watch-history.py")`.
47+
Use `mcp__nanoclaw__fetch_trakt_history()`.
4848
Saves fresh watch history to `/workspace/group/trakt-history.json`.
4949
Silent on success. Report on error or `total_shows: 0` (if sync hasn't run yet → skip silently).
5050

tiles/nanoclaw-admin/skills/trakt-watch-history/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
name: trakt-watch-history
3-
description: Fetch Trakt.tv watch history (shows, movies, ratings) for TV/movie recommendations. Runs host-side via run_host_script. Use when the user asks for show recommendations, what to watch, or wants to see their watch history.
3+
description: Fetch Trakt.tv watch history (shows, movies, ratings) for TV/movie recommendations. Use when the user asks for show recommendations, what to watch, or wants to see their watch history.
44
---
55

66
# Trakt Watch History
77

8-
Run via host: `mcp__nanoclaw__run_host_script(script: "trakt-watch-history.py")`
8+
Run via host: `mcp__nanoclaw__fetch_trakt_history()`
99

1010
The script returns JSON:
1111
```json

0 commit comments

Comments
 (0)