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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"sdk:test": "jest --testPathIgnorePatterns /node_modules/ tests/e2e/ --passWithNoTests",
"check:egress": "node scripts/check-egress.cjs",
"check:doc-version": "node scripts/check-doc-version.cjs",
"check:workspace": "node scripts/check-workspace.cjs",
"test:prod-bundle-key": "node scripts/check-prod-supabase-bundle.mjs",
"verify": "tsc --noEmit && jest --testPathIgnorePatterns /node_modules/ tests/e2e/ --passWithNoTests",
"test:wrapper-readiness": "node tests/e2e/wrapper-reach-preflight.mjs && jest tests/e2e/wrapper-readiness.test.ts",
Expand Down
48 changes: 48 additions & 0 deletions scripts/check-workspace.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env node
'use strict';
/**
* Refuse repo-specific work in the WRONG checkout — a durable guard for the launcher-misrouting defect
* (DECISION 3, #679 sprint). The session launcher has bound a TrustShell task to the old `ihh-ica` project
* while the working dir was the `trustshell-tl` worktree, so `start_session` resolved the wrong repo and
* work risked landing (or silently failing) against the wrong history. The session-binding itself is
* app-level config this script cannot change; this is the in-repo assertion so any loop or session can gate
* its own work and fail LOUDLY instead of silently misrouting:
*
* node scripts/check-workspace.cjs # expects origin = DealAppSeo/trustshell
* node scripts/check-workspace.cjs owner/repo # expects that repo
* TRUSTSHELL_EXPECT_REPO=owner/repo node scripts/check-workspace.cjs
*
* Exit 0 when origin matches, 1 (with a loud message) otherwise. No deps; safe to call at task startup.
*/
const { execSync } = require('node:child_process');

/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
const m = String(remoteUrl)
.trim()
.match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}
Comment on lines +19 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds the redaction helper used by the failure message

Suggested change
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
const m = String(remoteUrl)
.trim()
.match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
const m = String(remoteUrl)
.trim()
.match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}
/** Strip any credentials embedded in a remote URL before it is written to a log. */
function redactRemote(remoteUrl) {
const s = String(remoteUrl || '').trim();
return s ? s.replace(/\/\/[^/@\s]+@/, '//<redacted>@') : '(none)';
}


/** Resolve the current origin + branch and assert it is `expected`. Returns the process exit code. */
function main(argv, env, run) {
const expected = argv[2] || env.TRUSTSHELL_EXPECT_REPO || 'DealAppSeo/trustshell';
const sh = run || ((cmd) => execSync(cmd, { encoding: 'utf8' }).trim());
let remote = '';
let branch = '';
try { remote = sh('git remote get-url origin'); } catch { /* no remote */ }
try { branch = sh('git rev-parse --abbrev-ref HEAD'); } catch { /* detached / no repo */ }
if (!workspaceMatches(remote, expected)) {
process.stderr.write(
`WORKSPACE MISROUTED: origin is "${remote || '(none)'}", expected ${expected}. ` +
`Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
);
Comment on lines +36 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Workspace guard prints credential-bearing git remote URL verbatim to stderr

Severity: LOW · CWE-532

The new workspace guard writes the raw output of git remote get-url origin into its failure message, so any credentials embedded in the remote URL are disclosed in full. When a checkout is cloned with a token in the URL — the common https://x-access-token:$TOKEN@github.com/owner/repo.git form used by CI runners and agent sandboxes — and the guard fires on a misroute, the token is emitted to stderr, which is captured in terminal scrollback, CI job logs, and agent session transcripts that are read by more parties than the local git config. The script exists precisely to run at task startup in automated sessions, and the misroute path is the one that fires in misconfigured environments, so the leak lands in exactly the logs that get shared and retained. The repository's other tooling already redacts secrets and strips tokens before logging, so this path is inconsistent with the project's own hygiene standard.

Suggested change
process.stderr.write(
`WORKSPACE MISROUTED: origin is "${remote || '(none)'}", expected ${expected}. ` +
`Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
);
process.stderr.write(
`WORKSPACE MISROUTED: origin is "${redactRemote(remote)}", expected ${expected}. ` +
`Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
);
Prompt to fix with AI
This is a security vulnerability found during a code review.

Vulnerability: Workspace guard prints credential-bearing git remote URL verbatim to stderr
Severity: LOW
CWE: CWE-532

The new workspace guard writes the raw output of `git remote get-url origin` into its failure message, so any credentials embedded in the remote URL are disclosed in full. When a checkout is cloned with a token in the URL — the common `https://x-access-token:$TOKEN@github.com/owner/repo.git` form used by CI runners and agent sandboxes — and the guard fires on a misroute, the token is emitted to stderr, which is captured in terminal scrollback, CI job logs, and agent session transcripts that are read by more parties than the local git config. The script exists precisely to run at task startup in automated sessions, and the misroute path is the one that fires in misconfigured environments, so the leak lands in exactly the logs that get shared and retained. The repository's other tooling already redacts secrets and strips tokens before logging, so this path is inconsistent with the project's own hygiene standard.

Location: scripts/check-workspace.cjs:36-39
Context: Failure message echoes the raw remote URL, including any embedded credentials
```
// Before:
    process.stderr.write(
      `WORKSPACE MISROUTED: origin is "${remote || '(none)'}", expected ${expected}. ` +
        `Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
    );
// After:
    process.stderr.write(
      `WORKSPACE MISROUTED: origin is "${redactRemote(remote)}", expected ${expected}. ` +
        `Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
    );
```

Location: scripts/check-workspace.cjs:19-25
Context: Adds the redaction helper used by the failure message
```
// Before:
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
  const m = String(remoteUrl)
    .trim()
    .match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
  return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}
// After:
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
  const m = String(remoteUrl)
    .trim()
    .match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
  return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}

/** Strip any credentials embedded in a remote URL before it is written to a log. */
function redactRemote(remoteUrl) {
  const s = String(remoteUrl || '').trim();
  return s ? s.replace(/\/\/[^/@\s]+@/, '//<redacted>@') : '(none)';
}
```

How to fix:
Redact the userinfo component of the remote URL before it is written to stderr, or print only the parsed `owner/repo` (which is all the message needs to be actionable). Apply the same redaction to any other diagnostic that echoes a remote URL, and consider a shared helper so future scripts cannot reintroduce the pattern.

Please fix this vulnerability. If you propose a fix, make it concise and minimal.

React 👍 / 👎 to tune Strix for this repo. A repo collaborator (or the PR author) can resolve this thread to dismiss the finding.

return 1;
}
process.stderr.write(`workspace OK: ${expected}${branch ? ` @ ${branch}` : ''}\n`);
return 0;
}

module.exports = { workspaceMatches, main };

if (require.main === module) process.exit(main(process.argv, process.env));
38 changes: 38 additions & 0 deletions tests/check-workspace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* The workspace guard refuses repo-specific work in the wrong checkout (DECISION 3, #679 sprint): the launcher
* bound a TrustShell task to the old ihh-ica project. `workspaceMatches` is pure; `main` is run with an injected
* shell so the test asserts both exit paths without depending on the checkout it runs in.
*/
const { workspaceMatches, main } = require('../scripts/check-workspace.cjs') as {
workspaceMatches: (remoteUrl: string, expected: string) => boolean;
main: (argv: string[], env: NodeJS.ProcessEnv, run?: (cmd: string) => string) => number;
};

describe('check-workspace: refuse the wrong checkout (DECISION 3, #679)', () => {
it('matches https and ssh forms of the expected repo, with or without .git / case / trailing slash', () => {
for (const u of [
'https://github.com/DealAppSeo/trustshell.git',
'https://github.com/DealAppSeo/trustshell',
'git@github.com:DealAppSeo/trustshell.git',
'https://github.com/dealappseo/TrustShell/',
]) {
expect(workspaceMatches(u, 'DealAppSeo/trustshell')).toBe(true);
}
});

it('rejects the ihh-ica misroute, a different repo, and empty/garbage remotes', () => {
expect(workspaceMatches('https://github.com/DealAppSeo/ihh-ica.git', 'DealAppSeo/trustshell')).toBe(false);
expect(workspaceMatches('git@github.com:DealAppSeo/repid-engine.git', 'DealAppSeo/trustshell')).toBe(false);
expect(workspaceMatches('', 'DealAppSeo/trustshell')).toBe(false);
expect(workspaceMatches('not-a-url', 'DealAppSeo/trustshell')).toBe(false);
});

it('main() exits 0 on a matching origin and 1 (loudly) on a misroute — injected shell, no real git', () => {
const asTrustshell = (cmd: string) => (cmd.includes('remote get-url') ? 'https://github.com/DealAppSeo/trustshell.git' : 'main');
const asIhhIca = (cmd: string) => (cmd.includes('remote get-url') ? 'https://github.com/DealAppSeo/ihh-ica.git' : 'ica-build');
expect(main(['node', 'check-workspace.cjs'], {} as NodeJS.ProcessEnv, asTrustshell)).toBe(0);
expect(main(['node', 'check-workspace.cjs'], {} as NodeJS.ProcessEnv, asIhhIca)).toBe(1);
// explicit expected-repo arg wins
expect(main(['node', 'check-workspace.cjs', 'DealAppSeo/repid-engine'], {} as NodeJS.ProcessEnv, (cmd) => (cmd.includes('remote get-url') ? 'git@github.com:DealAppSeo/repid-engine.git' : 'main'))).toBe(0);
});
});
Loading