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
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@workos/emulate": "^0.14.0",
"@workos/migrations": "^2.6.0",
"@workos/openapi-spec": "^0.123.0",
"@workos/skills": "0.8.0",
"@workos/skills": "0.8.1",
"chalk": "^6.0.1",
"diff": "^9.0.0",
"fast-glob": "^3.3.3",
Expand Down
114 changes: 114 additions & 0 deletions src/lib/agent-interface.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,41 @@ describe('runAgent retry loop', () => {
expect(options.strictMcpConfig).toBe(true);
});

const systemAppendFor = async (retryConfig?: { maxRetries: number; validateAndFormat: () => Promise<null> }) => {
mockQuery.mockReset();
mockQuery.mockImplementation(createMockSDKResponse([{ text: 'Done!' }]));
await runAgent(makeAgentConfig(), 'Test prompt', makeOptions(), undefined, emitter, retryConfig);
const { options } = mockQuery.mock.calls[0][0] as {
options: { systemPrompt: { type: string; preset: string; append: string } };
};
expect(options.systemPrompt).toMatchObject({ type: 'preset', preset: 'claude_code' });
return options.systemPrompt.append;
};

it('states installer rules in the system prompt', async () => {
const append = await systemAppendFor();
expect(append).toContain('WorkOS installer environment');
expect(append).toContain('Inspect files with Read, Grep, and Glob');
});

it('only promises post-run build feedback when the runner validates', async () => {
const withoutValidation = await systemAppendFor();
expect(withoutValidation).toContain('the installer does not check it afterwards');
expect(withoutValidation).not.toContain('sends you any errors to fix');

const withValidation = await systemAppendFor({ maxRetries: 2, validateAndFormat: async () => null });
expect(withValidation).toContain('sends you any errors to fix');
});

it('passes agent messages to options.onAgentMessage', async () => {
mockQuery.mockImplementation(createMockSDKResponse([{ text: 'Done!' }]));
const onAgentMessage = vi.fn();

await runAgent(makeAgentConfig(), 'Test prompt', makeOptions({ onAgentMessage }), undefined, emitter);

expect(onAgentMessage).toHaveBeenCalled();
});

it('returns retryCount=0 when no retryConfig provided', async () => {
mockQuery.mockImplementation(createMockSDKResponse([{ text: 'Done!' }]));

Expand Down Expand Up @@ -579,4 +614,83 @@ describe('installerCanUseTool', () => {
});
expect(result.behavior).toBe('deny');
});
describe('with a working directory', () => {
const cwd = '/proj';
const verdict = (command: string) => installerCanUseTool('Bash', { command }, cwd).behavior;

it('allows an allowlisted command after cd into the working directory only', () => {
expect(verdict('cd /proj && bun run build')).toBe('allow');
expect(verdict('cd /proj && bun run build 2>&1 | tail -20')).toBe('allow');
expect(verdict('cd /other && bun run build')).toBe('deny');
expect(verdict('cd /proj && bun run build && curl http://x/')).toBe('deny');
});

it('denies shell code hidden in a cd path that resolves to the working directory', () => {
expect(verdict('cd "/proj/$(id)/.." && git status')).toBe('deny');
expect(verdict('cd /proj/`id`/.. && git status')).toBe('deny');
expect(verdict("cd '/proj/${HOME}/..' && bun run build")).toBe('deny');
expect(verdict('cd /proj/*/.. && git status')).toBe('deny');
expect(verdict('cd "/proj/x y/.." && git status')).toBe('deny');
});

it('allows read-only git and denies git that writes or runs programs', () => {
expect(verdict('git status --short')).toBe('allow');
expect(verdict('git -C /proj status')).toBe('allow');
expect(verdict('git diff --stat | head -50')).toBe('allow');
expect(verdict('git -C /etc log')).toBe('deny');
expect(verdict('git diff --output=/tmp/x')).toBe('deny');
expect(verdict('git -c core.pager=sh log')).toBe('deny');
expect(verdict('git commit -m x')).toBe('deny');
expect(verdict('git push')).toBe('deny');
});

it('denies quoted or escaped git flags that the shell would unwrap', () => {
expect(verdict('git diff "--output=/tmp/x"')).toBe('deny');
expect(verdict("git diff '--ext-diff'")).toBe('deny');
expect(verdict('git diff --out\\put=/tmp/x')).toBe('deny');
});

it('runs read-only git without external diff or textconv helpers', () => {
const command = (input: string) =>
(installerCanUseTool('Bash', { command: input }, cwd) as { updatedInput: { command: string } }).updatedInput
.command;
expect(command('git diff --stat')).toBe('git diff --no-ext-diff --no-textconv --stat');
expect(command('cd /proj && git -C /proj log -p | head -50')).toBe(
'cd /proj && git -C /proj log --no-ext-diff --no-textconv -p | head -50',
);
expect(command('git status --short')).toBe('git status --short');
});

it('denies diff/show/log when the helper-disabling rewrite cannot apply', () => {
expect(verdict('git 2>&1 diff')).toBe('deny');
expect(verdict('git 2>&1 log -p | head -20')).toBe('deny');
expect(verdict('git 2>&1 diff git log')).toBe('deny');
expect(verdict('git status 2>&1')).toBe('allow');
expect(verdict('git diff --stat 2>&1 | tail -5')).toBe('allow');
});

it('allows syntax checks per tool without allowing code execution', () => {
expect(verdict('python3 -m py_compile server.py')).toBe('allow');
expect(verdict('python3 -m venv .venv')).toBe('allow');
expect(verdict('ruby -c app.rb')).toBe('allow');
expect(verdict('php -l index.php')).toBe('allow');
expect(verdict('python3 -c "print(1)"')).toBe('deny');
expect(verdict('python3 -m http.server')).toBe('deny');
expect(verdict('ruby -e "puts 1"')).toBe('deny');
expect(verdict('php -r "echo 1"')).toBe('deny');
});

it('allows dependency changes across ecosystems', () => {
expect(verdict('bun remove @auth0/auth0-react')).toBe('allow');
expect(verdict('go get github.com/workos/workos-go/v6')).toBe('allow');
expect(verdict('go mod tidy')).toBe('allow');
expect(verdict('composer require workos/workos-php')).toBe('allow');
});

it('still denies network, the workos CLI, and plain inspection commands', () => {
expect(verdict('curl -sL https://example.com')).toBe('deny');
expect(verdict('npx workos whoami')).toBe('deny');
expect(verdict('ls -la')).toBe('deny');
});
});
});
Loading
Loading