Skip to content
Open
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
8 changes: 4 additions & 4 deletions config/governance-guidelines.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,24 @@
},
{
"id": "editorial-agent-output",
"version": "1.0.0",
"version": "1.0.1",
"status": "active",
"type": "contract",
"owner": "content",
"sourcePath": "docs/specs/editorial-agent-output-contract.md",
"sha256": "f150dc26b1cfe911521f2cf6207f70e6bfd75b7d5b8019e95e7830224fd4c274",
"sha256": "bd158110bdddd209fa0d4a9277f715fb9d79f34a6e36fc59275ae20f425b4bcf",
"appliesTo": ["editorial-agents", "editorial-workflows"],
"enforcement": "automated-blocking",
"evidence": ["validated editorial agent output"]
},
{
"id": "executable-codex-agents",
"version": "1.2.0",
"version": "1.2.1",
"status": "active",
"type": "standard",
"owner": "architecture",
"sourcePath": "docs/specs/executable-codex-agents.md",
"sha256": "b46c517ee0e39a35cf3c0d38b68e008bd4c8dd44bc193261e7d97dae35dc5d8a",
"sha256": "e12392121a16bc90a008e932fde0c673aa64edfc91db2e932847645cdcb3c5d0",
"appliesTo": ["agent-configuration", "agent-workflows"],
"enforcement": "automated-blocking",
"evidence": ["agent configuration validation"]
Expand Down
5 changes: 5 additions & 0 deletions docs/specs/editorial-agent-output-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ que inicia processo Codex separado com `CODEX_HOME` temporario, sem heranca de
ferramentas ou connectors da sessao pai. Antes da execucao, o runner exige
`codex mcp list --json` vazio e bloqueia qualquer preflight invalido.

O runner confirma `codex login status` como ChatGPT, copia somente o cache local
da sessao para o diretorio temporario com permissao restrita e o remove ao
encerrar. `OPENAI_API_KEY` nao e encaminhada nem usada; se a sessao ChatGPT ou
o cache local nao estiverem disponiveis, a execucao falha sem fallback.

O runner recebe `--agent`, `--input` e `--output`. A entrada segue
`config/editorial-agent-input.schema.json` e e um documento com `scenarioId`,
`inputState` e `input`; a saida gravada inclui o mesmo
Expand Down
5 changes: 5 additions & 0 deletions docs/specs/executable-codex-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ processo Codex separado com `CODEX_HOME` temporario e nao herda ferramentas ou
connectors da sessao pai. O runner tambem exige `codex mcp list --json` vazio e
falha fechado quando qualquer preflight ou validacao falhar.

A autenticacao usa somente uma sessao local confirmada como ChatGPT. O runner
copia temporariamente o cache local para o `CODEX_HOME` isolado, restringe sua
permissao e o remove ao encerrar. A chave `OPENAI_API_KEY` nao e encaminhada;
ausencia de sessao ou cache invalido bloqueia a execucao, sem fallback.

O modelo central exige `mcpPolicy: none` explicitamente para todos os adapters.
O validador rejeita a omissao da politica e qualquer servidor MCP configurado.

Expand Down
117 changes: 105 additions & 12 deletions scripts/run-editorial-agent.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ const ALLOWED_ENVIRONMENT = [
'LC_ALL',
'TERM',
'NO_COLOR',
'OPENAI_API_KEY',
];
const AUTH_CACHE_FILE = 'auth.json';

function argument(name) {
const index = process.argv.indexOf(name);
Expand Down Expand Up @@ -67,6 +67,73 @@ function isolatedEnvironment(tempHome) {
return environment;
}

function sourceEnvironment() {
const environment = {};
[
...ALLOWED_ENVIRONMENT,
'HOME',
'USERPROFILE',
'LOCALAPPDATA',
'CODEX_HOME',
].forEach((name) => {
if (process.env[name]) environment[name] = process.env[name];
});
return environment;
}

function authenticatedChatGptCache(codexBin) {
const status = spawnSync(codexBin, ['login', 'status'], {
cwd: ROOT,
env: sourceEnvironment(),
encoding: 'utf8',
});
const statusText = `${status.stdout || ''}\n${status.stderr || ''}`;
if (status.status !== 0 || !/logged in using chatgpt/i.test(statusText)) {
throw new Error('sessao ChatGPT do Codex indisponivel');
}

const sourceHome =
process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
const authPath = path.join(sourceHome, AUTH_CACHE_FILE);
let authStat;
try {
authStat = fs.lstatSync(authPath);
} catch {
throw new Error('cache local da sessao ChatGPT indisponivel');
}
if (!authStat.isFile() || authStat.isSymbolicLink()) {
throw new Error('cache local da sessao ChatGPT invalido');
}
return authPath;
}

function copyAuthenticatedCache(sourcePath, tempHome) {
const targetPath = path.join(tempHome, AUTH_CACHE_FILE);
fs.copyFileSync(sourcePath, targetPath, fs.constants.COPYFILE_EXCL);
fs.chmodSync(targetPath, 0o600);
}

function safeExecutionFailure(result) {
const details =
`${result.stdout || ''}\n${result.stderr || ''}`.toLowerCase();
if (/\b401\b/.test(details)) return 'resposta 401 do servico';
const categories = [
[
/authentication|unauthorized|not authenticated|login required|logged out|invalid (?:bearer |refresh )?token|expired token|\b403\b/,
'autenticacao',
],
[/network|connect|timeout|econn|dns|socket/, 'rede'],
[/sandbox|permission|operation not permitted/, 'sandbox'],
[/model|not available|unsupported/, 'modelo'],
[/schema|json/, 'schema'],
];
const category = categories.find(([pattern]) => pattern.test(details))?.[1];
if (category) return `falha de ${category}`;
if (result.error?.code) return `spawn ${result.error.code}`;
if (result.signal) return `sinal ${result.signal}`;
return `codigo ${result.status}`;
}

function buildMcpArguments() {
return ['mcp', 'list', '--json'];
}
Expand Down Expand Up @@ -121,18 +188,34 @@ function run() {
return fail('inputState invalido');
}

const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'quiz-codex-home-'));
const tempWorkspace = fs.mkdtempSync(
path.join(os.tmpdir(), 'quiz-editorial-workspace-'),
);
const tempMessage = path.join(tempHome, 'agent-output.json');
const env = isolatedEnvironment(tempHome);
let tempHome;
let tempWorkspace;
const cleanup = () => {
fs.rmSync(tempHome, { recursive: true, force: true });
fs.rmSync(tempWorkspace, { recursive: true, force: true });
if (tempHome) fs.rmSync(tempHome, { recursive: true, force: true });
if (tempWorkspace)
fs.rmSync(tempWorkspace, { recursive: true, force: true });
};
try {
if (!env.OPENAI_API_KEY) return fail('OPENAI_API_KEY nao configurada');
const sourceAuthPath = authenticatedChatGptCache(codexBin);
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'quiz-codex-home-'));
tempWorkspace = fs.mkdtempSync(
path.join(os.tmpdir(), 'quiz-editorial-workspace-'),
);
copyAuthenticatedCache(sourceAuthPath, tempHome);
const tempMessage = path.join(tempHome, 'agent-output.json');
const env = isolatedEnvironment(tempHome);
const isolatedLogin = spawnSync(codexBin, ['login', 'status'], {
cwd: tempWorkspace,
env,
encoding: 'utf8',
});
const isolatedStatus = `${isolatedLogin.stdout || ''}\n${isolatedLogin.stderr || ''}`;
if (
isolatedLogin.status !== 0 ||
!/logged in using chatgpt/i.test(isolatedStatus)
) {
return fail('cache ChatGPT nao autenticado no perfil isolado');
}
const mcpCheck = spawnSync(codexBin, buildMcpArguments(), {
cwd: tempWorkspace,
env,
Expand Down Expand Up @@ -163,7 +246,10 @@ function run() {
buildExecArguments(agentConfig, tempMessage, tempWorkspace, prompt),
{ cwd: tempWorkspace, env, encoding: 'utf8' },
);
if (result.status !== 0) return fail('execucao do agente falhou');
if (result.status !== 0) {
const detail = safeExecutionFailure(result);
return fail(`execucao do agente falhou (${detail})`);
}
let output;
try {
output = JSON.parse(fs.readFileSync(tempMessage, 'utf8'));
Expand Down Expand Up @@ -193,4 +279,11 @@ function run() {

if (require.main === module) run();

module.exports = { buildExecArguments, buildMcpArguments, run };
module.exports = {
authenticatedChatGptCache,
buildExecArguments,
buildMcpArguments,
copyAuthenticatedCache,
run,
safeExecutionFailure,
};
Loading
Loading