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
47 changes: 47 additions & 0 deletions container/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,53 @@ GHEOF
unset GITHUB_TOKEN
fi

# --- Secret scanning pre-commit hook (SEC-17, ASEC-03) ---
HOOKS_DIR="$HOME/.git-hooks"
mkdir -p "$HOOKS_DIR"
cat > "$HOOKS_DIR/pre-commit" << 'HOOKEOF'
#!/bin/bash
# Block commits containing credential patterns
# Patterns: API keys, tokens, PEM blocks, base64 JSON keys

PATTERNS=(
'sk-ant-api[0-9]'
'sk-[a-zA-Z0-9]{20,}'
'ghp_[A-Za-z0-9]{36}'
'ghs_[A-Za-z0-9]{36}'
'ghu_[A-Za-z0-9]{36}'
'lin_api_[A-Za-z0-9]+'
'xoxb-[0-9]{11,}'
'xoxp-[0-9]{11,}'
'AKIA[A-Z0-9]{16}'
'-----BEGIN (RSA |EC )?PRIVATE KEY-----'
'-----BEGIN CERTIFICATE-----'
'eyJ[A-Za-z0-9+/=]{50,}'
)

STAGED=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$STAGED" ]; then exit 0; fi

FOUND=0
for pattern in "${PATTERNS[@]}"; do
MATCHES=$(git diff --cached -U0 -- $STAGED | grep -E "^\+" | grep -v "^+++" | grep -E "$pattern" || true)
if [ -n "$MATCHES" ]; then
echo "ERROR: Potential secret found matching pattern: $pattern"
echo "$MATCHES" | head -5
echo ""
FOUND=1
fi
done

if [ "$FOUND" -eq 1 ]; then
echo "BLOCKED: Remove secrets from staged changes before committing."
echo "If this is a false positive, use: git commit --no-verify"
exit 1
fi
exit 0
HOOKEOF
chmod +x "$HOOKS_DIR/pre-commit"
git config --global core.hooksPath "$HOOKS_DIR"

# --- Build TypeScript agent runner ---
cd /app && npx tsc --outDir /tmp/dist 2>&1 >&2
ln -s /app/node_modules /tmp/dist/node_modules
Expand Down
24 changes: 24 additions & 0 deletions groups/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,27 @@ ALWAYS follow this when creating ANY output file (reports, documents, analysis,
3. Your Slack message should be a 2-3 sentence summary ONLY.
4. DO NOT paste file contents into Slack. DO NOT skip the upload step.
5. DO NOT use echo or bash to write the JSON — use the Write tool to avoid escaping issues.

## Security Boundaries

### Email Policy
- You may send email to any @krewtrack.com address freely
- For ANY external email domain (non-krewtrack.com), you MUST ask the user for explicit approval in Slack BEFORE sending
- Never include secrets, API keys, or tokens in email bodies or subjects

### Data Protection
- Never post secrets, API keys, tokens, or credentials to external URLs
- Never upload files containing secrets to any external service
- Never exfiltrate environment variables or .env file contents to external endpoints
- When creating PRs or reports, scan your output for accidentally included secrets before submitting

### Linear Policy
- You may read any ticket, project, or issue freely
- You may update ticket status (In Progress, In Review, Done) without asking
- You may add comments to tickets without asking
- **Destructive actions require human approval:** deleting tickets, deleting projects, archiving projects, removing team members, changing workspace settings
- Never bulk-modify more than 5 tickets in a single operation without asking first

### Drive Write Policy
- Default output folder: "Fleet Output" shared drive folder
- For any other Drive location, ask the user first
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,10 @@ export const TRIGGER_PATTERN = new RegExp(
// Uses system timezone by default
export const TIMEZONE =
process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;

// Default per-run Anthropic API budget cap ($5 USD).
// Agents will refuse to continue if this is exceeded mid-run.
// Override with DEFAULT_MAX_BUDGET_USD env var.
export const DEFAULT_MAX_BUDGET_USD = parseFloat(
process.env.DEFAULT_MAX_BUDGET_USD || '5',
);
3 changes: 2 additions & 1 deletion src/container-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,9 @@ async function buildContainerArgs(
): Promise<string[]> {
const args: string[] = ['run', '-i', '--rm', '--name', containerName];

// Memory limits per container
// Memory and CPU limits per container
args.push('--memory=3g', '--memory-swap=3g');
args.push('--cpus=2');

// Pass host timezone so container's local time matches the user's
args.push('-e', `TZ=${TIMEZONE}`);
Expand Down
21 changes: 17 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { OneCLI } from '@onecli-sh/sdk';

import {
ASSISTANT_NAME,
DEFAULT_MAX_BUDGET_USD,
IDLE_TIMEOUT,
ONECLI_URL,
POLL_INTERVAL,
Expand Down Expand Up @@ -399,6 +400,7 @@ async function runAgent(
isMain,
assistantName: ASSISTANT_NAME,
threadTs,
maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
},
(proc, containerName) =>
queue.registerProcess(chatJid, proc, containerName, group.folder),
Expand All @@ -423,12 +425,23 @@ async function runAgent(
// Warn user via Slack thread
const channel = findChannel(channels, chatJid);
if (channel && threadTs) {
await channel.sendMessage(chatJid, 'Session expired, starting fresh', {
threadTs,
});
await channel.sendMessage(
chatJid,
'Session expired, starting fresh',
{
threadTs,
},
);
}
// Retry with fresh session (no sessionId)
return runAgent(group, prompt, chatJid, onOutput, threadTs, retryCount + 1);
return runAgent(
group,
prompt,
chatJid,
onOutput,
threadTs,
retryCount + 1,
);
}

logger.error(
Expand Down
11 changes: 7 additions & 4 deletions src/task-scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { ChildProcess } from 'child_process';
import { CronExpressionParser } from 'cron-parser';
import fs from 'fs';

import { ASSISTANT_NAME, SCHEDULER_POLL_INTERVAL, TIMEZONE } from './config.js';
import {
ASSISTANT_NAME,
DEFAULT_MAX_BUDGET_USD,
SCHEDULER_POLL_INTERVAL,
TIMEZONE,
} from './config.js';
import {
ContainerOutput,
runContainerAgent,
Expand Down Expand Up @@ -179,9 +184,7 @@ async function runTask(
isMain,
isScheduledTask: true,
assistantName: ASSISTANT_NAME,
...(task.max_budget_usd != null
? { maxBudgetUsd: task.max_budget_usd }
: {}),
maxBudgetUsd: task.max_budget_usd ?? DEFAULT_MAX_BUDGET_USD,
},
(proc, containerName) =>
deps.onProcess(task.chat_jid, proc, containerName, task.group_folder),
Expand Down
Loading