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
16 changes: 15 additions & 1 deletion .agent/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@
}
]
},
"plan-guard": {
"enabled": true,
"PreToolUse": [
{
"matcher": "run_command",
"hooks": [
{
"type": "command",
"command": "node hooks/plan-guard.mjs",
"timeout": 10
}
]
}
]
},
"context-anchor": {
"enabled": true,
"PreInvocation": [
Expand All @@ -25,4 +40,3 @@
]
}
}

107 changes: 53 additions & 54 deletions .agent/hooks/branch-guard.mjs
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
#!/usr/bin/env node
/**
* ==============================================================================
* Agentic Android Delivery Kernel β€” Native Branch Guard Hook (PreToolUse)
* ==============================================================================
* Intercepts write_to_file and replace_file_content tool calls.
* Blocks modifications to repository source files if the current git branch
* matches the default branch (e.g. main/master), enforcing Gate 1.4 and Rule 0.
* ==============================================================================
*/

import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

function getRepoRoot() {
try {
return execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
} catch {
return process.cwd();
}
}

function getBlockedBranches() {
try {
const configPath = path.resolve(process.cwd(), 'kernel.config.json');
const configPath = path.resolve(getRepoRoot(), 'kernel.config.json');
if (fs.existsSync(configPath)) {
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const def = cfg?.git?.defaultBranch || 'main';
Expand All @@ -25,54 +24,54 @@ function getBlockedBranches() {
return ['main', 'master'];
}

let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => { input += chunk; });
function isArtifactPath(targetFile, repoRoot) {
if (!targetFile || typeof targetFile !== 'string') return false;
const resolved = path.resolve(repoRoot, targetFile);
const rel = path.relative(repoRoot, resolved);
if (!rel.startsWith('..') && !path.isAbsolute(rel)) return false;

process.stdin.on('end', () => {
try {
const payload = JSON.parse(input || '{}');
const toolCall = payload.toolCall || {};
const args = toolCall.args || {};
const targetFile = args.TargetFile || '';
return resolved.includes('/.gemini/antigravity/brain/') ||
resolved.includes('/.system_generated/') ||
Boolean(process.env.APP_DATA_DIR && resolved.startsWith(path.resolve(process.env.APP_DATA_DIR)));
}

// Allow writes to Antigravity brain artifacts, system logs, or scratchpads
const isArtifact = targetFile.includes('/.gemini/antigravity/brain/') ||
targetFile.includes('/.system_generated/') ||
targetFile.endsWith('implementation_plan.md') ||
targetFile.endsWith('walkthrough.md');
function inspectFileWrite(targetFile, currentBranch, blockedBranches, repoRoot) {
if (isArtifactPath(targetFile, repoRoot)) return { allow: true };

if (isArtifact) {
process.stdout.write(JSON.stringify({ decision: 'allow' }));
process.exit(0);
}
if (blockedBranches.includes(currentBranch)) {
return {
allow: false,
reason: `[Rule 0 Violation S-04] Direct modification to '${targetFile}' on '${currentBranch}' is blocked. Cut a dedicated feature branch first.`
};
}
return { allow: true };
}

// Determine current git branch
let currentBranch = '';
const isMain = Boolean(process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]));
if (isMain) {
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => { input += chunk; });
process.stdin.on('end', () => {
try {
currentBranch = execSync('git branch --show-current', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim();
} catch {
currentBranch = '';
}
const payload = JSON.parse(input || '{}');
const targetFile = payload?.toolCall?.args?.TargetFile || '';
const repoRoot = getRepoRoot();
let currentBranch = '';
try {
currentBranch = execSync('git branch --show-current', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
} catch {}

const blockedBranches = getBlockedBranches();
if (blockedBranches.includes(currentBranch)) {
const response = {
decision: 'deny',
reason: `[Rule 0 Violation] Direct modification to '${targetFile}' on branch '${currentBranch}' is strictly blocked.\n` +
`You must complete Phase 1 Inception, post the 4-Pillar Spec, obtain explicit written approval at Gate 1.4, ` +
`and cut a dedicated branch (<type>/issue-<id>-<slug>) before modifying repository files.`
};
process.stdout.write(JSON.stringify(response));
process.exit(0);
const res = inspectFileWrite(targetFile, currentBranch, getBlockedBranches(), repoRoot);
if (!res.allow) {
process.stdout.write(JSON.stringify({ decision: 'deny', reason: res.reason }));
process.exit(0);
}
process.stdout.write(JSON.stringify({ decision: 'allow' }));
} catch {
process.stdout.write(JSON.stringify({ decision: 'allow' }));
}
});
}

process.stdout.write(JSON.stringify({ decision: 'allow' }));
} catch {
// Fail-safe to allow in case of unparseable payload
process.stdout.write(JSON.stringify({ decision: 'allow' }));
}
});
export { isArtifactPath, inspectFileWrite, getBlockedBranches, getRepoRoot };
144 changes: 144 additions & 0 deletions .agent/hooks/plan-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env node
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

function getRepoRoot() {
try {
return execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
} catch {
return process.cwd();
}
}

function getBlockedBranches() {
try {
const configPath = path.resolve(getRepoRoot(), 'kernel.config.json');
if (fs.existsSync(configPath)) {
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const def = cfg?.git?.defaultBranch || 'main';
return [def, 'main', 'master'].filter((v, i, a) => a.indexOf(v) === i);
}
} catch {}
return ['main', 'master'];
}

function tokenize(cmd) {
return cmd.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
}

function inspectCommand(commandLine, currentBranch, blockedBranches) {
if (!commandLine || typeof commandLine !== 'string') return { allow: true };

// 1. Hook Path Tampering: Block overriding core.hooksPath or GIT_HOOKS_PATH
if (/-c\s*core\.hooksPath/i.test(commandLine) ||
/\bGIT_HOOKS_PATH\b/i.test(commandLine) ||
/\bcore\.hooksPath\s*=/i.test(commandLine)) {
return {
allow: false,
reason: "[Security Violation S-02] Tampering with 'core.hooksPath' is strictly prohibited. Pre-commit airbag cannot be deactivated."
};
}

const segments = commandLine.split(/(?:&&|\|\||[;\n|&])/).map(s => s.trim()).filter(Boolean);

for (const segment of segments) {
const tokens = tokenize(segment);
let i = 0;
while (i < tokens.length && /^[a-zA-Z_]\w*=/.test(tokens[i])) i++;

if (i >= tokens.length || tokens[i] !== 'git') continue;
i++;

const flagsWithArg = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--super-prefix', '--exec-path', '--config-env']);
let subcommand = null;
let subArgs = [];

while (i < tokens.length) {
const t = tokens[i];
if (t.startsWith('--')) {
i += (!t.includes('=') && flagsWithArg.has(t)) ? 2 : 1;
} else if (t.startsWith('-')) {
i += flagsWithArg.has(t) ? 2 : 1;
} else {
subcommand = t;
subArgs = tokens.slice(i + 1);
break;
}
}

if (!subcommand) continue;

if (subcommand === 'commit') {
const hasNoVerify = subArgs.some(a => (!a.startsWith('"') && !a.startsWith("'")) && (a === '--no-verify' || /^-[a-zA-Z]*n[a-zA-Z]*$/.test(a)));
if (hasNoVerify) {
return {
allow: false,
reason: "[Airbag Bypass S-02] 'git commit' with '--no-verify' or '-n' is strictly prohibited. All commits must pass the airbag."
};
}

if (blockedBranches.includes(currentBranch)) {
return {
allow: false,
reason: `[Rule 0 Violation S-01] Direct 'git commit' on protected branch '${currentBranch}' is blocked. Cut a dedicated feature branch first.`
};
}
}

if (subcommand === 'push') {
if (subArgs.some(a => a === '--no-verify')) {
return { allow: false, reason: "[Airbag Bypass S-02] 'git push' with '--no-verify' is strictly prohibited." };
}

if (blockedBranches.includes(currentBranch)) {
return { allow: false, reason: `[Security Violation S-01] Direct 'git push' from protected branch '${currentBranch}' is strictly blocked.` };
}

const targetsBlocked = subArgs.some(a => {
const c = a.replace(/^['"]|['"]$/g, '');
return blockedBranches.includes(c) || c.endsWith(':main') || c.endsWith(':master') || c === 'origin/main' || c === 'origin/master';
});

if (targetsBlocked) {
return { allow: false, reason: "[Security Violation S-01] Direct 'git push' targeting protected branch 'main'/'master' is strictly prohibited." };
}
}

if (subcommand === 'merge' && blockedBranches.includes(currentBranch)) {
return { allow: false, reason: `[Rule 0 Violation S-01] Direct 'git merge' on protected branch '${currentBranch}' is blocked. Merge via PR at Gate 3.5.` };
}
}

return { allow: true };
}

const isMain = Boolean(process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]));
if (isMain) {
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => { input += chunk; });
process.stdin.on('end', () => {
try {
const payload = JSON.parse(input || '{}');
const commandLine = payload?.toolCall?.args?.CommandLine || payload?.toolCall?.args?.command || payload?.toolCall?.args?.Command || '';

let currentBranch = '';
try {
currentBranch = execSync('git branch --show-current', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
} catch {}

const res = inspectCommand(commandLine, currentBranch, getBlockedBranches());
if (!res.allow) {
process.stdout.write(JSON.stringify({ decision: 'deny', reason: res.reason }));
process.exit(0);
}
process.stdout.write(JSON.stringify({ decision: 'allow' }));
} catch {
process.stdout.write(JSON.stringify({ decision: 'allow' }));
}
});
}

export { inspectCommand, tokenize, getBlockedBranches, getRepoRoot };
2 changes: 1 addition & 1 deletion .agent/personas/p4-system-architect.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ tools:
- find_by_name
- grep_search
- list_dir
- run_command (read-only verification: test scripts & rules)
- GitHubMCP:add_issue_comment
deny:
- write_to_file
- replace_file_content
- run_command
- GitHubMCP:create_issue
- GitHubMCP:create_pull_request
- GitHubMCP:merge_pull_request
Expand Down
Loading
Loading