Skip to content

Commit 268e130

Browse files
committed
🔧 update (workflow): merge dev pipeline changes
2 parents d866933 + 6ca1e65 commit 268e130

8 files changed

Lines changed: 131 additions & 9 deletions

File tree

.github/workflows/landing.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
run: bun run --cwd src/landing build
3232

3333
- name: Upload artifact
34-
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
34+
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
3535
with:
3636
path: src/landing/dist
3737

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# ── Stage 1: Install + Build ────────────────────────────────────────
2-
FROM oven/bun:1.3.11 AS builder
2+
FROM oven/bun:1.3.13 AS builder
33

44
WORKDIR /app
55

@@ -45,7 +45,7 @@ COPY . .
4545
RUN bun run build
4646

4747
# ── Stage 2: Production ─────────────────────────────────────────────
48-
FROM oven/bun:1.3.11-slim AS production
48+
FROM oven/bun:1.3.13-slim AS production
4949

5050
WORKDIR /app
5151

packages/config/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@
3333
"@tinyclaw/types": "workspace:*",
3434
"@tinyclaw/logger": "workspace:*",
3535
"@wgtechlabs/config-engine": "^0.1.0",
36-
"zod": "^3.24.0"
36+
"zod": "^4.4.3"
3737
}
3838
}

packages/shield/src/matcher.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ export interface MatchResult {
4141
matchValue: string;
4242
}
4343

44+
// ---------------------------------------------------------------------------
45+
// Constants
46+
// ---------------------------------------------------------------------------
47+
48+
/**
49+
* Tool argument keys that carry document payloads (not SQL parameters).
50+
* Excluded from SQL injection scanning to prevent false positives on
51+
* document writes (e.g. write_file with `content`, chat with `message`).
52+
*/
53+
const CONTENT_FIELDS: ReadonlySet<string> = new Set(['content', 'body', 'message', 'text']);
54+
4455
// ---------------------------------------------------------------------------
4556
// Directive parsing
4657
// ---------------------------------------------------------------------------
@@ -142,7 +153,17 @@ function evaluateCondition(
142153

143154
// "tool.call with arguments containing SQL syntax (DROP, DELETE, UNION, --)"
144155
if (lc.includes('arguments containing')) {
145-
const argsStr = JSON.stringify(event.toolArgs ?? {}).toLowerCase();
156+
// Only scan fields that could be SQL injection vectors.
157+
// Exclude content-body fields (document payloads written to files,
158+
// not SQL parameters) to prevent false positives.
159+
const rawArgs = event.toolArgs ?? {};
160+
const filteredArgs: Record<string, unknown> = {};
161+
for (const [key, val] of Object.entries(rawArgs)) {
162+
if (!CONTENT_FIELDS.has(key)) {
163+
filteredArgs[key] = val;
164+
}
165+
}
166+
const argsStr = JSON.stringify(filteredArgs).toLowerCase();
146167
// Extract keywords from parenthetical list
147168
const openIdx = condition.indexOf('(');
148169
const closeIdx = openIdx >= 0 ? condition.indexOf(')', openIdx + 1) : -1;
@@ -152,7 +173,13 @@ function evaluateCondition(
152173
.split(',')
153174
.map((k) => k.trim().toLowerCase());
154175
for (const keyword of keywords) {
155-
if (keyword && argsStr.includes(keyword)) {
176+
if (!keyword) continue;
177+
if (keyword === '--') {
178+
// Match SQL comment marker (--) but NOT markdown separators (---)
179+
if (/(?<!-)--(?!-)/.test(argsStr)) {
180+
return { matchedOn: 'tool.args', matchValue: keyword };
181+
}
182+
} else if (argsStr.includes(keyword)) {
156183
return { matchedOn: 'tool.args', matchValue: keyword };
157184
}
158185
}

packages/shield/tests/matcher.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,62 @@ describe('matchEvent — tool.call', () => {
127127
expect(matches).toHaveLength(0);
128128
});
129129

130+
it('should not match -- in markdown separators (---) inside content fields', () => {
131+
const threat = makeThreat({
132+
recommendationAgent:
133+
'BLOCK: tool.call with arguments containing SQL syntax (DROP, DELETE, UNION, --)',
134+
});
135+
136+
const event: ShieldEvent = {
137+
scope: 'tool.call',
138+
toolName: 'heartware_write',
139+
toolArgs: {
140+
filename: 'FRIEND.md',
141+
content:
142+
'# About My Owner\n\n- **Name:** Waren\n\n---\nThis file helps me understand you better.',
143+
},
144+
};
145+
146+
const matches = matchEvent(event, [threat]);
147+
expect(matches).toHaveLength(0);
148+
});
149+
150+
it('should not false-positive on content-body fields containing SQL words', () => {
151+
const threat = makeThreat({
152+
recommendationAgent:
153+
'BLOCK: tool.call with arguments containing SQL syntax (DROP, DELETE, UNION, --)',
154+
});
155+
156+
const event: ShieldEvent = {
157+
scope: 'tool.call',
158+
toolName: 'heartware_write',
159+
toolArgs: {
160+
filename: 'MEMORY.md',
161+
content: 'User asked me to delete the old notes and drop the schedule',
162+
},
163+
};
164+
165+
const matches = matchEvent(event, [threat]);
166+
expect(matches).toHaveLength(0);
167+
});
168+
169+
it('should still block SQL keywords in non-content args', () => {
170+
const threat = makeThreat({
171+
recommendationAgent:
172+
'BLOCK: tool.call with arguments containing SQL syntax (DROP, DELETE, UNION, --)',
173+
});
174+
175+
const event: ShieldEvent = {
176+
scope: 'tool.call',
177+
toolName: 'db_query',
178+
toolArgs: { query: 'DROP TABLE users; -- pwned' },
179+
};
180+
181+
const matches = matchEvent(event, [threat]);
182+
expect(matches.length).toBeGreaterThan(0);
183+
expect(matches[0].matchedOn).toBe('tool.args');
184+
});
185+
130186
it('should match compatible scope/category', () => {
131187
const threat = makeThreat({
132188
category: 'prompt', // prompt category

src/cli/src/commands/start.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,18 @@ export async function startCommand(): Promise<void> {
13761376
const gateway = createGateway();
13771377

13781378
// Register web UI as a channel sender (SSE push)
1379-
gateway.register('web', webUI.getChannelSender());
1379+
const webSender = webUI.getChannelSender();
1380+
gateway.register('web', webSender);
1381+
1382+
// If the owner was claimed via CLI setup, the persisted ownerId has
1383+
// prefix "cli:" but only a "web" channel is registered. Register a
1384+
// "cli" alias so nudges for "cli:owner" route through the web sender.
1385+
if (persistedOwnerId?.startsWith('cli:') && !gateway.getRegisteredChannels().includes('cli')) {
1386+
gateway.register('cli', {
1387+
...webSender,
1388+
name: `${webSender.name} (cli alias)`,
1389+
});
1390+
}
13801391

13811392
// --- Nudge Engine -------------------------------------------------------
13821393

src/cli/tests/commands/start.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,11 @@ const mockWebUIStop = mock(() => Promise.resolve());
272272

273273
// ── Mock @tinyclaw/gateway ────────────────────────────────────────────
274274

275+
const mockGatewayRegister = mock(() => {});
276+
275277
mock.module('@tinyclaw/gateway', () => ({
276278
createGateway: mock(() => ({
277-
register: mock(() => {}),
279+
register: mockGatewayRegister,
278280
unregister: mock(() => {}),
279281
send: mock(() => Promise.resolve({ success: true, channel: 'web', userId: 'web:owner' })),
280282
broadcast: mock(() => Promise.resolve([])),
@@ -375,6 +377,7 @@ beforeEach(() => {
375377
readyTag: 'Tiny Claw#1234',
376378
lastError: null,
377379
}));
380+
mockGatewayRegister.mockClear();
378381
});
379382

380383
afterEach(() => {
@@ -395,6 +398,31 @@ describe('startCommand', () => {
395398
expect(mockWebUIStart).toHaveBeenCalled();
396399
});
397400

401+
test('registers cli channel alias for cli-prefixed owner', async () => {
402+
await startCommand();
403+
const registeredChannels = mockGatewayRegister.mock.calls.map(
404+
([channel]: [string, ...unknown[]]) => channel,
405+
);
406+
expect(registeredChannels).toContain('cli');
407+
});
408+
409+
test('does not register cli channel alias for non-cli-prefixed owner', async () => {
410+
mockConfigGet.mockImplementation((key: string) => {
411+
if (key === 'providers.starterBrain.model') return 'kimi-k2.5:cloud';
412+
if (key === 'providers.starterBrain.baseUrl') return 'https://ollama.com';
413+
if (key === 'heartware.seed') return 42;
414+
if (key === 'owner.ownerId') return 'web:owner';
415+
if (key === 'channels.discord.enabled') return true;
416+
if (key === 'plugins.enabled') return ['@tinyclaw/plugin-channel-discord'];
417+
return undefined;
418+
});
419+
await startCommand();
420+
const registeredChannels = mockGatewayRegister.mock.calls.map(
421+
([channel]: [string, ...unknown[]]) => channel,
422+
);
423+
expect(registeredChannels).not.toContain('cli');
424+
});
425+
398426
test('initializes heartware', async () => {
399427
await startCommand();
400428
expect(mockHeartwareInitialize).toHaveBeenCalled();

src/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"@tinyclaw/logger": "workspace:*",
2424
"@tinyclaw/types": "workspace:*",
2525
"dompurify": "^3.2.6",
26-
"marked": "^17.0.3",
26+
"marked": "^18.0.0",
2727
"qrcode": "^1.5.4",
2828
"svelte": "^5.20.1"
2929
},

0 commit comments

Comments
 (0)