boxsafe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.directory +2 -0
- package/.env.example +3 -0
- package/AUDIT_LANG.md +45 -0
- package/BOXSAFE_VERSION_NOTES.md +14 -0
- package/README.md +4 -0
- package/TODO.md +130 -0
- package/adapters/index.ts +27 -0
- package/adapters/primary/cli-adapter.ts +56 -0
- package/adapters/secondary/filesystem/node-filesystem.ts +307 -0
- package/adapters/secondary/system/configuration.ts +147 -0
- package/ai/caller.ts +42 -0
- package/ai/label.ts +33 -0
- package/ai/modelConfig.ts +236 -0
- package/ai/provider.ts +111 -0
- package/boxsafe.config.json +68 -0
- package/core/auth/dasktop/cred/CRED.md +112 -0
- package/core/auth/dasktop/cred/credLinux.ts +82 -0
- package/core/auth/dasktop/cred/credWin.ts +2 -0
- package/core/config/defaults/boxsafeDefaults.ts +67 -0
- package/core/config/defaults/index.ts +1 -0
- package/core/config/loadConfig.ts +133 -0
- package/core/loop/about.md +13 -0
- package/core/loop/boxConfig.ts +20 -0
- package/core/loop/buildExecCommand.ts +76 -0
- package/core/loop/cmd/execode.ts +121 -0
- package/core/loop/cmd/test.js +3 -0
- package/core/loop/execLoop.ts +341 -0
- package/core/loop/git/VERSIONING.md +17 -0
- package/core/loop/git/commands.ts +11 -0
- package/core/loop/git/gitClient.ts +78 -0
- package/core/loop/git/index.ts +99 -0
- package/core/loop/git/runVersionControlRunner.ts +33 -0
- package/core/loop/initNavigator.ts +44 -0
- package/core/loop/initTasksManager.ts +35 -0
- package/core/loop/runValidation.ts +25 -0
- package/core/loop/tasks/AGENT-TASKS.md +36 -0
- package/core/loop/tasks/index.ts +96 -0
- package/core/loop/toolCalls.ts +168 -0
- package/core/loop/toolDispatcher.ts +146 -0
- package/core/loop/traceLogger.ts +106 -0
- package/core/loop/types.ts +26 -0
- package/core/loop/versionControlAdapter.ts +36 -0
- package/core/loop/waterfall.ts +404 -0
- package/core/loop/writeArtifactAtomically.ts +13 -0
- package/core/navigate/NAVIGATE.md +186 -0
- package/core/navigate/about.md +128 -0
- package/core/navigate/examples.ts +367 -0
- package/core/navigate/handler.ts +148 -0
- package/core/navigate/index.ts +32 -0
- package/core/navigate/navigate.test.ts +372 -0
- package/core/navigate/navigator.ts +437 -0
- package/core/navigate/types.ts +132 -0
- package/core/navigate/utils.ts +146 -0
- package/core/paths/paths.ts +33 -0
- package/core/ports/index.ts +271 -0
- package/core/segments/CONVENTIONS.md +30 -0
- package/core/segments/loop/index.ts +18 -0
- package/core/segments/map.ts +56 -0
- package/core/segments/navigate/index.ts +20 -0
- package/core/segments/versionControl/index.ts +18 -0
- package/core/util/logger.ts +128 -0
- package/docs/AGENT-TASKS.md +36 -0
- package/docs/ARQUITETURA_CORRECAO.md +121 -0
- package/docs/CONVENTIONS.md +30 -0
- package/docs/CRED.md +112 -0
- package/docs/L_RAG.md +567 -0
- package/docs/NAVIGATE.md +186 -0
- package/docs/PRIMARY_ACTORS.md +78 -0
- package/docs/SECONDARY_ACTORS.md +174 -0
- package/docs/VERSIONING.md +17 -0
- package/docs/boxsafe.config.md +472 -0
- package/eslint.config.mts +15 -0
- package/main.ts +53 -0
- package/memo/generated/codelog.md +13 -0
- package/memo/state/tasks/state.json +6 -0
- package/memo/state/tasks/tasks/task_001.md +2 -0
- package/memo/states-logs/logs.txt +7 -0
- package/memo/states-logs/trace-mljvrxvi-9g0k4q.jsonl +11 -0
- package/memo/states-logs/trace-mljvvc9j-pe9ekj.jsonl +11 -0
- package/memo/states-logs/trace-mljvvm1c-wbnqzp.jsonl +11 -0
- package/memo/states-logs/trace-mljxecwn-9xh3nw.jsonl +11 -0
- package/memo/states-logs/trace-mljxqkfm-ipijik.jsonl +11 -0
- package/memo/states-logs/trace-mljxwtrw-3fanky.jsonl +11 -0
- package/memo/states-logs/trace-mljxzen3-m8iinh.jsonl +11 -0
- package/memo/states-logs/trace-mljyucef-td6odn.jsonl +11 -0
- package/memo/states-logs/trace-mljyuprw-b1a6f4.jsonl +11 -0
- package/memo/states-logs/trace-mljyvefl-b6yoce.jsonl +11 -0
- package/memo/states-logs/trace-mljyxjo4-n7ibj2.jsonl +13 -0
- package/memo/states-logs/trace-mljziez5-8drqtn.jsonl +13 -0
- package/memo/states-logs/trace-mljziulp-dtd03z.jsonl +13 -0
- package/memo/states-logs/trace-mljzjwrq-1p2krb.jsonl +13 -0
- package/memo/states-logs/trace-mljzl0i7-b1cqa6.jsonl +13 -0
- package/memo/states-logs/trace-mljzmlk6-7kdyls.jsonl +13 -0
- package/memo/states-logs/trace-mlk0oj25-xa3dcu.jsonl +13 -0
- package/memo/states-logs/trace-mlk1x59q-713huj.jsonl +14 -0
- package/memo/states-logs/trace-mlk22dz8-7fd6hq.jsonl +14 -0
- package/memo/states-logs/trace-mlk241uy-wmx907.jsonl +14 -0
- package/memo/states-logs/trace-mlk2bf5r-yoh1vg.jsonl +15 -0
- package/package.json +44 -0
- package/pnpm-workspace.yaml +4 -0
- package/prompt_improvement_example.md +55 -0
- package/remove.txt +1 -0
- package/tests/adapters.test.ts +128 -0
- package/tests/extractCode.test.ts +26 -0
- package/tests/integration.test.ts +83 -0
- package/tests/loadConfig.test.ts +25 -0
- package/tests/navigatorBoundary.test.ts +17 -0
- package/tests/ports.test.ts +84 -0
- package/tests/runAllTests.ts +49 -0
- package/tests/toolCalls.test.ts +149 -0
- package/tests/waterfall.test.ts +52 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +17 -0
- package/types.d.ts +96 -0
- package/util/ANSI.ts +29 -0
- package/util/extractCode.ts +217 -0
- package/util/extractToolCalls.ts +80 -0
- package/util/logger.ts +125 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Credenciais Linux (credLinux)
|
|
2
|
+
|
|
3
|
+
Módulo para gerenciar credenciais usando o **secret-tool** do Linux (keyring nativo).
|
|
4
|
+
|
|
5
|
+
## Requisitos
|
|
6
|
+
|
|
7
|
+
- Linux com `secret-tool` instalado (já vem na maioria das distros)
|
|
8
|
+
- Funciona em qualquer desktop environment (GNOME, KDE, XFCE, etc.)
|
|
9
|
+
|
|
10
|
+
## Importação
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { saveCredLinux, getCredLinux, deleteCredLinux } from '@memo/desktop/pass';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Funções
|
|
17
|
+
|
|
18
|
+
### `saveCredLinux()`
|
|
19
|
+
|
|
20
|
+
Salva uma credencial no keyring do sistema.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
const success = await saveCredLinux({
|
|
24
|
+
password: 'minha-senha-secreta',
|
|
25
|
+
label: 'GitHub Token',
|
|
26
|
+
account: 'gh-token',
|
|
27
|
+
service: 'meu-app' // opcional, default: 'box-safe'
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Retorna: true (sucesso) | false (erro)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `getCredLinux()`
|
|
34
|
+
|
|
35
|
+
Recupera uma credencial salva.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const token = await getCredLinux({
|
|
39
|
+
account: 'gh-token',
|
|
40
|
+
service: 'meu-app' // opcional, default: 'box-safe'
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Retorna: string (senha) | null (não encontrada)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### `deleteCredLinux()`
|
|
47
|
+
|
|
48
|
+
Remove uma credencial do keyring.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const deleted = await deleteCredLinux({
|
|
52
|
+
account: 'gh-token',
|
|
53
|
+
service: 'meu-app' // opcional, default: 'box-safe'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Retorna: true (sucesso) | false (erro)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Exemplo Completo
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Salvar API key
|
|
63
|
+
await saveCredLinux({
|
|
64
|
+
password: 'sk-xxxxxxxxxxxxx',
|
|
65
|
+
label: 'OpenAI API Key',
|
|
66
|
+
account: 'openai-key'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Usar depois
|
|
70
|
+
const apiKey = await getCredLinux({ account: 'openai-key' });
|
|
71
|
+
if (apiKey) {
|
|
72
|
+
console.log('Key encontrada:', apiKey);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Limpar quando não precisar mais
|
|
76
|
+
await deleteCredLinux({ account: 'openai-key' });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Parâmetros
|
|
80
|
+
|
|
81
|
+
### CredentialArgs (save)
|
|
82
|
+
- `password`: senha/token a salvar
|
|
83
|
+
- `label`: descrição amigável (aparece na GUI do keyring)
|
|
84
|
+
- `account`: identificador único da credencial
|
|
85
|
+
- `service`: (opcional) nome do app/serviço (default: `box-safe`)
|
|
86
|
+
|
|
87
|
+
### LookupArgs (get/delete)
|
|
88
|
+
- `account`: identificador da credencial
|
|
89
|
+
- `service`: (opcional) nome do app/serviço (default: `box-safe`)
|
|
90
|
+
|
|
91
|
+
## Onde ficam salvos?
|
|
92
|
+
|
|
93
|
+
As credenciais são criptografadas e salvas em:
|
|
94
|
+
```
|
|
95
|
+
~/.local/share/keyrings/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Você pode visualizar usando `seahorse` (GNOME) ou a GUI do seu keyring.
|
|
99
|
+
|
|
100
|
+
## Logs
|
|
101
|
+
|
|
102
|
+
O módulo só loga erros no console. Se algo falhar:
|
|
103
|
+
- `saveCredLinux`: retorna `false`
|
|
104
|
+
- `getCredLinux`: retorna `null`
|
|
105
|
+
- `deleteCredLinux`: retorna `false`
|
|
106
|
+
|
|
107
|
+
Tratamento de erro fica por sua conta.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## WINDOWS
|
|
111
|
+
for windows we use keytar
|
|
112
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/***
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Manages credentials using Linux secret-tool
|
|
4
|
+
* @module
|
|
5
|
+
* memo/desktop/pass
|
|
6
|
+
***/
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import { ANSI } from '@util/ANSI';
|
|
10
|
+
import { Logger } from '@core/util/logger';
|
|
11
|
+
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
|
|
14
|
+
interface CredentialArgs {
|
|
15
|
+
password: string;
|
|
16
|
+
label: string;
|
|
17
|
+
account: string;
|
|
18
|
+
service?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface LookupArgs {
|
|
22
|
+
account: string;
|
|
23
|
+
service?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_SERVICE = 'box-safe';
|
|
27
|
+
const logger = Logger.createModuleLogger('CredentialManager');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Saves credential to the system keyring
|
|
31
|
+
*/
|
|
32
|
+
export async function setCredLinux({
|
|
33
|
+
password,
|
|
34
|
+
label,
|
|
35
|
+
account,
|
|
36
|
+
service = DEFAULT_SERVICE,
|
|
37
|
+
}: CredentialArgs): Promise<boolean> {
|
|
38
|
+
try {
|
|
39
|
+
await execAsync(
|
|
40
|
+
`echo -n "${password}" | secret-tool store --label="${label}" service ${service} account ${account}`
|
|
41
|
+
);
|
|
42
|
+
return true;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
logger.error(`Failed to save credential: ${err}`);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retrieves credential from the keyring
|
|
51
|
+
*/
|
|
52
|
+
export async function getCredLinux({
|
|
53
|
+
account,
|
|
54
|
+
service = DEFAULT_SERVICE,
|
|
55
|
+
}: LookupArgs): Promise<string | null> {
|
|
56
|
+
try {
|
|
57
|
+
const { stdout } = await execAsync(
|
|
58
|
+
`secret-tool lookup service ${service} account ${account}`
|
|
59
|
+
);
|
|
60
|
+
return stdout.trim();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger.error(`Credential not found: ${account}`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Deletes credential from the keyring
|
|
69
|
+
*/
|
|
70
|
+
export async function deleteCredLinux({
|
|
71
|
+
account,
|
|
72
|
+
service = DEFAULT_SERVICE,
|
|
73
|
+
}: LookupArgs): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
await execAsync(`secret-tool clear service ${service} account ${account}`);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
logger.error(`Failed to delete credential: ${account}`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Types will be imported from loadConfig to avoid external dependencies
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_BOXSAFE_CONFIG = {
|
|
4
|
+
project: {
|
|
5
|
+
workspace: './',
|
|
6
|
+
testDir: './',
|
|
7
|
+
versionControl: {
|
|
8
|
+
before: false,
|
|
9
|
+
after: false,
|
|
10
|
+
generateNotes: false,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
model: {
|
|
14
|
+
primary: {
|
|
15
|
+
provider: 'google',
|
|
16
|
+
name: 'gemini-2.5-flash',
|
|
17
|
+
},
|
|
18
|
+
fallback: [],
|
|
19
|
+
endpoint: null,
|
|
20
|
+
parameters: {},
|
|
21
|
+
},
|
|
22
|
+
smartRotation: {
|
|
23
|
+
enabled: false,
|
|
24
|
+
simple: [],
|
|
25
|
+
complex: [],
|
|
26
|
+
},
|
|
27
|
+
limits: {
|
|
28
|
+
tokens: 100000,
|
|
29
|
+
loops: 10,
|
|
30
|
+
timeout: {
|
|
31
|
+
enabled: false,
|
|
32
|
+
duration: '1h',
|
|
33
|
+
notify: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
sandbox: {
|
|
37
|
+
enabled: true,
|
|
38
|
+
engine: 'docker',
|
|
39
|
+
memory: '512m',
|
|
40
|
+
cpu: 0.5,
|
|
41
|
+
network: 'none',
|
|
42
|
+
},
|
|
43
|
+
commands: {
|
|
44
|
+
setup: 'npm install',
|
|
45
|
+
run: 'echo OK',
|
|
46
|
+
test: null,
|
|
47
|
+
timeoutMs: 60_000,
|
|
48
|
+
},
|
|
49
|
+
interface: {
|
|
50
|
+
channel: 'terminal',
|
|
51
|
+
prompt: '',
|
|
52
|
+
notifications: {
|
|
53
|
+
whatsapp: false,
|
|
54
|
+
telegram: false,
|
|
55
|
+
slack: false,
|
|
56
|
+
email: false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
paths: {
|
|
60
|
+
generatedMarkdown: './memo/generated/codelog.md',
|
|
61
|
+
artifactOutput: './out.ts',
|
|
62
|
+
},
|
|
63
|
+
teach: {
|
|
64
|
+
urls: [],
|
|
65
|
+
files: [],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DEFAULT_BOXSAFE_CONFIG } from './boxsafeDefaults';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_BOXSAFE_CONFIG } from '@core/config/defaults';
|
|
4
|
+
|
|
5
|
+
// Local types to maintain core independence
|
|
6
|
+
export interface BoxSafeConfig {
|
|
7
|
+
project?: {
|
|
8
|
+
workspace?: string;
|
|
9
|
+
testDir?: string;
|
|
10
|
+
versionControl?: {
|
|
11
|
+
before?: boolean;
|
|
12
|
+
after?: boolean;
|
|
13
|
+
autoPush?: boolean;
|
|
14
|
+
generateNotes?: boolean;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
model?: {
|
|
18
|
+
primary?: {
|
|
19
|
+
provider?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
};
|
|
22
|
+
fallback?: any[];
|
|
23
|
+
endpoint?: any;
|
|
24
|
+
parameters?: Record<string, any>;
|
|
25
|
+
};
|
|
26
|
+
smartRotation?: {
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
simple?: any[];
|
|
29
|
+
complex?: any[];
|
|
30
|
+
};
|
|
31
|
+
limits?: {
|
|
32
|
+
tokens?: number;
|
|
33
|
+
loops?: number;
|
|
34
|
+
timeout?: {
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
duration?: string;
|
|
37
|
+
notify?: boolean;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
sandbox?: {
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
engine?: string;
|
|
43
|
+
memory?: string;
|
|
44
|
+
cpu?: number;
|
|
45
|
+
network?: string;
|
|
46
|
+
};
|
|
47
|
+
commands?: {
|
|
48
|
+
setup?: string;
|
|
49
|
+
run?: string;
|
|
50
|
+
test?: string | null;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
};
|
|
53
|
+
interface?: {
|
|
54
|
+
channel?: string;
|
|
55
|
+
prompt?: string;
|
|
56
|
+
notifications?: {
|
|
57
|
+
whatsapp?: boolean;
|
|
58
|
+
telegram?: boolean;
|
|
59
|
+
slack?: boolean;
|
|
60
|
+
email?: boolean;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
paths?: {
|
|
64
|
+
generatedMarkdown?: string;
|
|
65
|
+
artifactOutput?: string;
|
|
66
|
+
};
|
|
67
|
+
teach?: {
|
|
68
|
+
urls?: string[];
|
|
69
|
+
files?: string[];
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type NormalizedBoxSafeConfig = Omit<BoxSafeConfig, 'limits'> & {
|
|
74
|
+
limits?: Omit<NonNullable<BoxSafeConfig['limits']>, 'loops'> & {
|
|
75
|
+
loops?: number;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type LoadConfigResult = {
|
|
80
|
+
config: NormalizedBoxSafeConfig;
|
|
81
|
+
source: { path: string; loaded: boolean };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
85
|
+
return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deepMerge<T>(base: T, override: unknown): T {
|
|
89
|
+
if (!isPlainObject(base) || !isPlainObject(override)) return (override ?? base) as T;
|
|
90
|
+
|
|
91
|
+
const out: Record<string, unknown> = { ...(base as any) };
|
|
92
|
+
for (const [k, v] of Object.entries(override)) {
|
|
93
|
+
const bv = (base as any)[k];
|
|
94
|
+
if (isPlainObject(bv) && isPlainObject(v)) out[k] = deepMerge(bv, v);
|
|
95
|
+
else out[k] = v;
|
|
96
|
+
}
|
|
97
|
+
return out as T;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeLoops(v: unknown, fallback: number): number {
|
|
101
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
102
|
+
if (typeof v === 'string') {
|
|
103
|
+
const trimmed = v.trim().toLowerCase();
|
|
104
|
+
if (trimmed === 'infinity') return Number.MAX_SAFE_INTEGER;
|
|
105
|
+
const n = Number(trimmed);
|
|
106
|
+
if (Number.isFinite(n)) return n;
|
|
107
|
+
}
|
|
108
|
+
return fallback;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function loadBoxSafeConfig(configPath?: string): LoadConfigResult {
|
|
112
|
+
const p = configPath ?? path.resolve(process.cwd(), 'boxsafe.config.json');
|
|
113
|
+
|
|
114
|
+
let rawConfig: unknown = null;
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(p)) {
|
|
117
|
+
rawConfig = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
rawConfig = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const merged = deepMerge(DEFAULT_BOXSAFE_CONFIG, rawConfig ?? {});
|
|
124
|
+
|
|
125
|
+
const loopsFallback = typeof DEFAULT_BOXSAFE_CONFIG.limits?.loops === 'number' ? DEFAULT_BOXSAFE_CONFIG.limits.loops : 2;
|
|
126
|
+
if (!merged.limits) merged.limits = {} as any;
|
|
127
|
+
(merged.limits as any).loops = normalizeLoops((merged.limits as any).loops, loopsFallback);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
config: merged as NormalizedBoxSafeConfig,
|
|
131
|
+
source: { path: p, loaded: Boolean(rawConfig) },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { loadBoxSafeConfig } from '@core/config/loadConfig';
|
|
2
|
+
import type { NormalizedBoxSafeConfig } from '@core/config/loadConfig';
|
|
3
|
+
|
|
4
|
+
export function loadBoxConfig(configPath?: string): NormalizedBoxSafeConfig {
|
|
5
|
+
return loadBoxSafeConfig(configPath).config;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getVersionControlFlags(boxConfig: NormalizedBoxSafeConfig): {
|
|
9
|
+
vcBefore: boolean;
|
|
10
|
+
vcAfter: boolean;
|
|
11
|
+
vcGenerateNotes: boolean;
|
|
12
|
+
vcAutoPushConfig: boolean;
|
|
13
|
+
} {
|
|
14
|
+
return {
|
|
15
|
+
vcBefore: Boolean(boxConfig.project?.versionControl?.before ?? false),
|
|
16
|
+
vcAfter: Boolean(boxConfig.project?.versionControl?.after ?? false),
|
|
17
|
+
vcGenerateNotes: Boolean(boxConfig.project?.versionControl?.generateNotes ?? false),
|
|
18
|
+
vcAutoPushConfig: Boolean(boxConfig.project?.versionControl?.autoPush ?? false),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Logger } from '@core/util/logger';
|
|
4
|
+
import { ANSI } from '@util/ANSI';
|
|
5
|
+
|
|
6
|
+
type AnsiLike = { Cyan: string; Yellow: string; Reset: string };
|
|
7
|
+
|
|
8
|
+
type BuildExecCommandArgs = {
|
|
9
|
+
cmd: any;
|
|
10
|
+
lang: string;
|
|
11
|
+
pathOutput: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const logger = Logger.createModuleLogger('BuildExecCommand');
|
|
15
|
+
|
|
16
|
+
export async function buildExecCommand({ cmd, lang, pathOutput }: BuildExecCommandArgs): Promise<any> {
|
|
17
|
+
// If caller left the default `echo OK`, automatically run the
|
|
18
|
+
// generated output file according to the requested language so the
|
|
19
|
+
// loop actually executes the produced artifact.
|
|
20
|
+
let execCmd = cmd;
|
|
21
|
+
if (typeof cmd === 'string' && cmd === 'echo OK') {
|
|
22
|
+
if (lang === 'ts') execCmd = ['tsx', [pathOutput]];
|
|
23
|
+
else if (lang === 'js') execCmd = ['node', [pathOutput]];
|
|
24
|
+
else if (lang === 'py' || lang === 'python') execCmd = ['python', [pathOutput]];
|
|
25
|
+
else if (lang === 'sh' || lang === 'bash' || lang === 'shell') execCmd = ['bash', [pathOutput]];
|
|
26
|
+
else execCmd = `${pathOutput}`;
|
|
27
|
+
|
|
28
|
+
const display = typeof execCmd === 'string' ? execCmd : Array.isArray(execCmd) ? `${execCmd[0]} ${execCmd[1].join(' ')}` : String(execCmd);
|
|
29
|
+
logger.info(`auto-executing generated file with: ${display}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If executing JS: check for CommonJS usage (require) and project type=module.
|
|
33
|
+
try {
|
|
34
|
+
const isNodeExec =
|
|
35
|
+
lang === 'js' &&
|
|
36
|
+
((typeof execCmd === 'string' && execCmd.trimStart().startsWith('node ')) ||
|
|
37
|
+
(Array.isArray(execCmd) && String(execCmd[0]) === 'node'));
|
|
38
|
+
|
|
39
|
+
if (isNodeExec) {
|
|
40
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
41
|
+
let isModuleType = false;
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(pkgPath)) {
|
|
44
|
+
const pkgRaw = fs.readFileSync(pkgPath, 'utf-8');
|
|
45
|
+
const pkg = JSON.parse(pkgRaw);
|
|
46
|
+
isModuleType = pkg.type === 'module';
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
isModuleType = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const outContent = fs.readFileSync(pathOutput, 'utf-8');
|
|
53
|
+
if (isModuleType && /\brequire\s*\(/.test(outContent) && path.extname(pathOutput) === '.js') {
|
|
54
|
+
const newPath = pathOutput.replace(/\.js$/, '.cjs');
|
|
55
|
+
try {
|
|
56
|
+
await fs.promises.rename(pathOutput, newPath);
|
|
57
|
+
execCmd = ['node', [newPath]];
|
|
58
|
+
logger.info(`renamed output to ${newPath} for CommonJS compatibility`);
|
|
59
|
+
// create a minimal ESM wrapper at the original path so artifact exists
|
|
60
|
+
try {
|
|
61
|
+
const wrapper = `import { spawnSync } from 'node:child_process';\nimport path from 'node:path';\nconst target = path.join(path.dirname(new URL(import.meta.url).pathname), '${path.basename(newPath)}');\nconst res = spawnSync('node', [target], { stdio: 'inherit' });\nif (res.error) throw res.error;\nprocess.exit(res.status ?? 0);\n`;
|
|
62
|
+
await fs.promises.writeFile(pathOutput, wrapper, 'utf-8');
|
|
63
|
+
} catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
logger.warn(`failed to rename file`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// non-fatal
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return execCmd;
|
|
76
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import type { CommandRun } from "../../../types";
|
|
4
|
+
import type { ExecResult } from "@core/loop/waterfall";
|
|
5
|
+
import { STATES_LOGS_DIR, STATES_LOG_FILE } from "@core/paths/paths";
|
|
6
|
+
|
|
7
|
+
type ExecodeOptions = {
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
allowUnsafeShell?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MAX_RUNTIME_MS = 60_000;
|
|
13
|
+
|
|
14
|
+
function getEffectiveTimeoutMs(timeoutMs?: number): number {
|
|
15
|
+
const fromEnv = process.env.BOXSAFE_CMD_TIMEOUT_MS ? Number(process.env.BOXSAFE_CMD_TIMEOUT_MS) : NaN;
|
|
16
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
|
|
17
|
+
if (typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0) return timeoutMs;
|
|
18
|
+
return DEFAULT_MAX_RUNTIME_MS;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function containsShellOperators(s: string): boolean {
|
|
22
|
+
return /[;&|`]|\$\(|\$\{|>>?|<\(|<|\n|\r/.test(s);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isObviouslyDangerousCommand(s: string): boolean {
|
|
26
|
+
const x = s.toLowerCase();
|
|
27
|
+
return (
|
|
28
|
+
/\brm\s+-rf\b/.test(x) ||
|
|
29
|
+
/\bmkfs\b/.test(x) ||
|
|
30
|
+
/\bdd\b\s+if=/.test(x) ||
|
|
31
|
+
/\bshutdown\b/.test(x) ||
|
|
32
|
+
/\breboot\b/.test(x)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldAllowUnsafeShell(options?: ExecodeOptions): boolean {
|
|
37
|
+
const fromEnv = process.env.BOXSAFE_ALLOW_UNSAFE_SHELL?.toLowerCase();
|
|
38
|
+
if (fromEnv === 'true' || fromEnv === '1' || fromEnv === 'yes') return true;
|
|
39
|
+
return Boolean(options?.allowUnsafeShell);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function execode(
|
|
43
|
+
command: CommandRun,
|
|
44
|
+
options?: ExecodeOptions
|
|
45
|
+
): Promise<ExecResult> {
|
|
46
|
+
const maxRuntimeMs = getEffectiveTimeoutMs(options?.timeoutMs);
|
|
47
|
+
const normalized = normalizeCommand(command);
|
|
48
|
+
const { cmd, args, useShell } = normalized;
|
|
49
|
+
|
|
50
|
+
if (useShell) {
|
|
51
|
+
const allowUnsafe = shouldAllowUnsafeShell(options);
|
|
52
|
+
if (!allowUnsafe) {
|
|
53
|
+
if (containsShellOperators(cmd) || isObviouslyDangerousCommand(cmd)) {
|
|
54
|
+
const stderr = `Blocked potentially unsafe shell command: ${cmd}`;
|
|
55
|
+
await mkdir(STATES_LOGS_DIR, { recursive: true });
|
|
56
|
+
await writeFile(STATES_LOG_FILE, stderr, "utf8");
|
|
57
|
+
return { exitCode: 126, stdout: "", stderr };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await mkdir(STATES_LOGS_DIR, { recursive: true });
|
|
63
|
+
|
|
64
|
+
return new Promise<ExecResult>((resolve, reject) => {
|
|
65
|
+
const child = spawn(cmd, args, { shell: useShell });
|
|
66
|
+
|
|
67
|
+
let stdout = "";
|
|
68
|
+
let stderr = "";
|
|
69
|
+
let timedOut = false;
|
|
70
|
+
|
|
71
|
+
child.stdout?.on("data", (data) => {
|
|
72
|
+
stdout += data.toString();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.stderr?.on("data", (data) => {
|
|
76
|
+
stderr += data.toString();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
timedOut = true;
|
|
81
|
+
child.kill("SIGTERM");
|
|
82
|
+
}, maxRuntimeMs);
|
|
83
|
+
|
|
84
|
+
child.on("error", (err) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
reject(err);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
child.on("close", async (code, signal) => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
|
|
92
|
+
const log = [
|
|
93
|
+
`exitCode=${code ?? 0}`,
|
|
94
|
+
`signal=${signal ?? "none"}`,
|
|
95
|
+
`timedOut=${timedOut}`,
|
|
96
|
+
`stdout:`, stdout,
|
|
97
|
+
`stderr:`, stderr,
|
|
98
|
+
].join("\n");
|
|
99
|
+
|
|
100
|
+
await writeFile(STATES_LOG_FILE, log, "utf8");
|
|
101
|
+
|
|
102
|
+
resolve({
|
|
103
|
+
exitCode: code ?? 0,
|
|
104
|
+
stdout,
|
|
105
|
+
stderr,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeCommand(
|
|
112
|
+
command: CommandRun
|
|
113
|
+
): { cmd: string; args: string[]; useShell: boolean } {
|
|
114
|
+
if (typeof command === "string") {
|
|
115
|
+
return { cmd: command, args: [], useShell: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const [cmd, args] = command;
|
|
119
|
+
return { cmd, args, useShell: false };
|
|
120
|
+
}
|
|
121
|
+
|