shraga 0.1.111 → 0.1.113
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/README.md +4 -1
- package/defaults/mcps/README.md +6 -3
- package/defaults/skills/mcp-server.md +12 -5
- package/defaults/skills/platform.md +4 -1
- package/dist/client/assets/index-DIDtPQb-.css +10 -0
- package/dist/client/assets/index-DJ0AgGIu.js +1969 -0
- package/dist/client/index.html +2 -2
- package/package.json +3 -2
- package/src/client/App.tsx +33 -8
- package/src/client/components/BackendStatusBanner.tsx +62 -0
- package/src/client/components/ConfigPanel.tsx +61 -15
- package/src/client/components/ConversationHeader.tsx +5 -1
- package/src/client/components/McpManager.tsx +26 -9
- package/src/client/components/SkillsManager.tsx +48 -27
- package/src/client/hooks/useAuth.ts +11 -2
- package/src/client/hooks/useIsOwner.ts +24 -0
- package/src/client/hooks/useModules.ts +5 -1
- package/src/client/lib/api.ts +21 -5
- package/src/client/lib/backendHealth.ts +230 -0
- package/src/client/lib/debug.ts +48 -0
- package/src/client/lib/sessionApi.ts +24 -8
- package/src/client/lib/ws.ts +21 -13
- package/src/scripts/harden-audit.sh +55 -0
- package/src/server/api-key-routes.ts +64 -0
- package/src/server/api-keys.ts +181 -43
- package/src/server/auth.ts +113 -47
- package/src/server/boot.ts +158 -104
- package/src/server/claude.ts +112 -4
- package/src/server/data-sync.ts +55 -6
- package/src/server/directives.ts +10 -5
- package/src/server/engine/claude-code.ts +190 -32
- package/src/server/engine/claude-resume.ts +205 -0
- package/src/server/engine/types.ts +10 -0
- package/src/server/hooks.ts +19 -0
- package/src/server/mcp-oauth.ts +24 -5
- package/src/server/mcp-server.ts +55 -25
- package/src/server/modules/routes.ts +2 -6
- package/src/server/notify-owners.ts +5 -17
- package/src/server/owners.ts +14 -0
- package/src/server/scheduler/builtins.ts +3 -1
- package/src/server/scheduler/runner.ts +3 -0
- package/src/server/security/audit.ts +498 -0
- package/src/server/security/enforce.ts +306 -0
- package/src/server/security/escalate.ts +194 -0
- package/src/server/security/guard.ts +329 -0
- package/src/server/security/owner-only.ts +15 -0
- package/src/server/security/owner-routes.ts +43 -0
- package/src/server/security/policy.ts +413 -0
- package/src/server/security/principal.ts +80 -0
- package/src/server/security/revocation.ts +50 -0
- package/src/server/security/runtime.ts +174 -0
- package/src/server/sessions.ts +51 -9
- package/src/server/shraga-config.ts +3 -0
- package/src/server/slack/bot.ts +44 -11
- package/src/server/slack/context-cache.ts +40 -7
- package/src/server/webhook-lane/feature.ts +17 -6
- package/src/shared/models.ts +11 -0
- package/dist/client/assets/index-BNAh4GUs.js +0 -1949
- package/dist/client/assets/index-DIMte_k6.css +0 -10
package/src/server/claude.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { summarizeText } from './summarize.ts';
|
|
|
3
3
|
import { dataSync } from './data-sync.ts';
|
|
4
4
|
import type { McpConfig } from './mcp.ts';
|
|
5
5
|
import type { ClaudeAccountRef } from './claude-account.ts';
|
|
6
|
-
import { loadConversation, saveConversation, appendMessage, getSession, setSessionDirectives, addTriggeredSkills, upsertSession, type ConvMessage, type ConvBlock } from './sessions.ts';
|
|
6
|
+
import { loadConversation, saveConversation, appendMessage, getSession, setSessionDirectives, addTriggeredSkills, upsertSession, setClaudeResume, getSessionFloor, lowerSessionFloor, type ConvMessage, type ConvBlock } from './sessions.ts';
|
|
7
|
+
import { enforcing, TurnGuard } from './security/enforce.ts';
|
|
7
8
|
import { createTurnAccumulator, type TurnStreamHooks } from './turn-stream.ts';
|
|
8
9
|
import {
|
|
9
10
|
resolveDefaultSkillsContent,
|
|
@@ -25,6 +26,8 @@ import { getUserContextBlock } from './user-context.ts';
|
|
|
25
26
|
import { collectTurnContext } from './turn-context.ts';
|
|
26
27
|
import { DATA_DIR, dataPath } from './paths.ts';
|
|
27
28
|
import * as contacts from './contacts.ts';
|
|
29
|
+
import type { Principal } from './security/principal.ts';
|
|
30
|
+
import { security, resolvePrincipal } from './security/runtime.ts';
|
|
28
31
|
import { resolveAndGetEngine, ModelUnavailableError } from './engine/index.ts';
|
|
29
32
|
|
|
30
33
|
const CONFIG_PATH = dataPath('agent-config.json');
|
|
@@ -197,7 +200,10 @@ function applyCompactMarkers(conv: ConvMessage[]): ConvMessage[] {
|
|
|
197
200
|
|
|
198
201
|
export interface AttachmentMeta { url: string; name: string; mimeType: string; path: string }
|
|
199
202
|
|
|
200
|
-
export
|
|
203
|
+
export interface StreamChatOpts {
|
|
204
|
+
/** Who this turn runs for. Required so every channel is enumerated by the compiler. Shadow mode:
|
|
205
|
+
* resolved + audited (turn.start/turn.end), not yet enforced. */
|
|
206
|
+
principal: Principal;
|
|
201
207
|
prompt: string;
|
|
202
208
|
attachments?: AttachmentMeta[];
|
|
203
209
|
images?: string[];
|
|
@@ -217,7 +223,85 @@ export async function* streamChat(opts: {
|
|
|
217
223
|
* where an add-on's contributor reads its own keys. */
|
|
218
224
|
turnHints?: Record<string, unknown>;
|
|
219
225
|
context?: Record<string, string>;
|
|
220
|
-
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Run one agent turn. Security wrapper: resolves the principal's role/profile and audits turn.start / turn.end.
|
|
229
|
+
* SHADOW (SECURITY_ENFORCE off): the turn is unchanged; auditing can never alter or break it.
|
|
230
|
+
* ENFORCE: the session floor is lowered to the caller's rank (taint), the turn runs under a TurnGuard whose
|
|
231
|
+
* effective profile the engine applies, and it FAILS CLOSED — no runtime, a resolution error, or an effective
|
|
232
|
+
* profile with `outbound: false` (e.g. `none`) refuses the turn with an `error` event. */
|
|
233
|
+
export async function* streamChat(opts: StreamChatOpts): AsyncGenerator<WsEvent> {
|
|
234
|
+
const sec = security();
|
|
235
|
+
const enforce = enforcing();
|
|
236
|
+
if (!sec) {
|
|
237
|
+
if (!enforce) { yield* runTurn(opts); return; }
|
|
238
|
+
console.error('[security] SECURITY_ENFORCE is on but the security runtime is not initialized — refusing the turn');
|
|
239
|
+
yield { type: 'error', message: 'Security enforcement is on but not initialized, so this turn was not run.' };
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const { principal, sessionId } = opts;
|
|
243
|
+
let role: string | undefined;
|
|
244
|
+
let guard: TurnGuard | undefined;
|
|
245
|
+
let refusal: string | undefined;
|
|
246
|
+
try {
|
|
247
|
+
const d = sec.decide(principal, sessionId);
|
|
248
|
+
role = d.role;
|
|
249
|
+
let enforced: Record<string, unknown> = {};
|
|
250
|
+
if (enforce) {
|
|
251
|
+
// A broken policy fails closed at resolve time already; it must not ALSO permanently taint every session it touches.
|
|
252
|
+
if (sessionId && sec.policy.valid) lowerSessionFloor(sessionId, d.rank);
|
|
253
|
+
guard = new TurnGuard({ runtime: sec, principal, sessionId, role: d.role, rank: d.rank, floorOf: getSessionFloor });
|
|
254
|
+
const eff = guard.current();
|
|
255
|
+
if (!eff.profile.outbound) refusal = `This conversation runs as role "${eff.role}", which is not permitted to run the agent.`;
|
|
256
|
+
enforced = { enforced: true, effectiveRole: eff.role, effectiveProfile: eff.profileName, floor: sessionId ? getSessionFloor(sessionId) : undefined };
|
|
257
|
+
}
|
|
258
|
+
sec.record({ type: 'turn.start', principal: principal.id, role, sessionId, meta: { kind: principal.kind, lane: principal.attrs.lane, source: opts.context?.source, profile: d.profile, rank: d.rank, wouldDeny: d.wouldDeny, ...enforced } });
|
|
259
|
+
} catch (e: any) {
|
|
260
|
+
console.error('[security] turn.start audit failed:', e.message);
|
|
261
|
+
if (enforce) refusal = 'The security check for this turn failed, so it was not run.';
|
|
262
|
+
}
|
|
263
|
+
const started = Date.now();
|
|
264
|
+
let outcome = 'closed';
|
|
265
|
+
try {
|
|
266
|
+
if (enforce && (refusal || !guard)) {
|
|
267
|
+
outcome = 'denied';
|
|
268
|
+
console.warn(`[security] Refused turn for ${principal.id} session=${sessionId ?? 'new'}: ${refusal}`);
|
|
269
|
+
yield { type: 'error', message: refusal ?? 'The security check for this turn failed, so it was not run.' };
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
for await (const ev of runTurn(opts, guard)) {
|
|
273
|
+
if (ev.type === 'done') outcome = 'done';
|
|
274
|
+
else if (ev.type === 'error') outcome = 'error';
|
|
275
|
+
yield ev;
|
|
276
|
+
}
|
|
277
|
+
if (outcome === 'closed') outcome = 'done';
|
|
278
|
+
} catch (e) {
|
|
279
|
+
outcome = 'threw';
|
|
280
|
+
throw e;
|
|
281
|
+
} finally {
|
|
282
|
+
try { sec.record({ type: 'turn.end', principal: principal.id, role, sessionId, reason: outcome, meta: { ms: Date.now() - started } }); }
|
|
283
|
+
catch (e: any) { console.error('[security] turn.end audit failed:', e.message); }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** SECURITY_ENFORCE: `principal`'s rank under the live policy. Undefined in shadow mode, without a runtime, or on an
|
|
288
|
+
* invalid (fail-closed) policy — callers then apply no rank-based filtering. */
|
|
289
|
+
export function enforcedRank(principal: Principal): number | undefined {
|
|
290
|
+
const sec = security();
|
|
291
|
+
return enforcing() && sec?.policy.valid ? resolvePrincipal(sec.policy, principal).rank : undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** SECURITY_ENFORCE: input from `principal` entered `sessionId` outside a turn it runs (e.g. another human's Slack
|
|
295
|
+
* thread message) — lower the session floor to that principal's rank. No-op in shadow mode, without a runtime, or
|
|
296
|
+
* on an invalid (fail-closed) policy. Returns the new floor, or undefined when nothing was done. */
|
|
297
|
+
export async function taintSession(sessionId: string, principal: Principal | (() => Promise<Principal>)): Promise<number | undefined> {
|
|
298
|
+
const sec = security();
|
|
299
|
+
if (!enforcing() || !sec?.policy.valid) return undefined;
|
|
300
|
+
const p = typeof principal === 'function' ? await principal() : principal;
|
|
301
|
+
return lowerSessionFloor(sessionId, resolvePrincipal(sec.policy, p).rank);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function* runTurn(opts: StreamChatOpts, guard?: TurnGuard): AsyncGenerator<WsEvent> {
|
|
221
305
|
const config = getAgentConfig();
|
|
222
306
|
const { prompt: cleanPrompt, directives: parsed, unresolvedModel } = parseDirectives(opts.prompt);
|
|
223
307
|
|
|
@@ -324,7 +408,10 @@ export async function* streamChat(opts: {
|
|
|
324
408
|
const stickyNames = opts.sessionId ? getSession(opts.sessionId)?.triggeredSkills ?? [] : [];
|
|
325
409
|
const newTriggerNames = discoveryEnabled ? matchTriggeredSkillNames(effectivePrompt, opts.context) : [];
|
|
326
410
|
const triggeredNames = [...new Set([...stickyNames, ...newTriggerNames])];
|
|
327
|
-
|
|
411
|
+
// Per-name blocks so an engine that resumes across turns can send only newly triggered ones. Joined
|
|
412
|
+
// exactly as skillInjectionBlocks(triggeredNames) joins them, so contextBlock is byte-identical.
|
|
413
|
+
const triggeredSkillBlocks = triggeredNames.map((n) => [n, skillInjectionBlocks([n])] as const).filter(([, b]) => b);
|
|
414
|
+
const triggeredSkills = triggeredSkillBlocks.map(([, b]) => b).join('\n');
|
|
328
415
|
|
|
329
416
|
// Per-skill turn budget. Resolved HERE, once every invoked skill is known (the slash command and
|
|
330
417
|
// the triggered/sticky set), and GAP-FILLING only: an inline `[turns:N]` or a session-pinned
|
|
@@ -347,6 +434,11 @@ export async function* streamChat(opts: {
|
|
|
347
434
|
const teamRoster = contacts.formatRoster();
|
|
348
435
|
const userContextBlock = getUserContextBlock(contact);
|
|
349
436
|
const contextBlock = [userBlock, userContextBlock, teamRoster, defaultSkills, triggeredSkills, skillIndex, mcpSkills, workspaceTree].filter(Boolean).join('\n');
|
|
437
|
+
const contextSections: Record<string, string> = Object.fromEntries(Object.entries({
|
|
438
|
+
user: userBlock, userContext: userContextBlock, roster: teamRoster, defaultSkills,
|
|
439
|
+
...Object.fromEntries(triggeredSkillBlocks.map(([n, b]) => [`skill:${n}`, b])),
|
|
440
|
+
skillIndex, mcpSkills, workspace: workspaceTree,
|
|
441
|
+
}).filter(([, v]) => v));
|
|
350
442
|
|
|
351
443
|
// Load conversation for the engine
|
|
352
444
|
const sessionId = opts.sessionId ?? crypto.randomUUID();
|
|
@@ -387,12 +479,27 @@ export async function* streamChat(opts: {
|
|
|
387
479
|
yield { type: 'error', message };
|
|
388
480
|
return;
|
|
389
481
|
}
|
|
482
|
+
// An engine that doesn't apply the profile can only run a turn that needs no restriction.
|
|
483
|
+
if (guard && !engine.enforcesProfile) {
|
|
484
|
+
const { profile, role } = guard.current();
|
|
485
|
+
if (!(profile.tools.includes('*') && profile.mcps.includes('*') && profile.env.includes('*'))) {
|
|
486
|
+
const message = `Engine "${engine.name}" does not enforce security profiles, so it cannot run this conversation's role "${role}".`;
|
|
487
|
+
console.error(`[security] ${message} (user=${opts.uid} session=${sessionId})`);
|
|
488
|
+
yield { type: 'error', message };
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
390
492
|
console.log(`[stream] engine=${engine.name} user=${opts.uid} session=${sessionId}`);
|
|
493
|
+
// Another engine's turn never reaches the claude-code transcript, so a stored SDK-resume mapping is
|
|
494
|
+
// stale from here on — mark it; the claude-code engine then falls back to a fresh query (engine-switch).
|
|
495
|
+
const resumeState = getSession(sessionId)?.claudeResume;
|
|
496
|
+
if (engine.name !== 'claude-code' && resumeState && !resumeState.interruptedBy) setClaudeResume(sessionId, undefined, { interruptedBy: engine.name });
|
|
391
497
|
|
|
392
498
|
yield* engine.stream({
|
|
393
499
|
prompt: turnPrompt,
|
|
394
500
|
conversation,
|
|
395
501
|
contextBlock,
|
|
502
|
+
contextSections,
|
|
396
503
|
attachments: opts.attachments,
|
|
397
504
|
images: opts.images,
|
|
398
505
|
sessionId,
|
|
@@ -408,6 +515,7 @@ export async function* streamChat(opts: {
|
|
|
408
515
|
turnHints: opts.turnHints,
|
|
409
516
|
conversationReset: opts.conversationReset,
|
|
410
517
|
context: opts.context,
|
|
518
|
+
...(guard ? { security: guard } : {}),
|
|
411
519
|
directives,
|
|
412
520
|
config,
|
|
413
521
|
});
|
package/src/server/data-sync.ts
CHANGED
|
@@ -4,9 +4,13 @@ import path from 'node:path';
|
|
|
4
4
|
import { DATA_DIR } from './paths.ts';
|
|
5
5
|
import { notifyOwners } from './notify-owners.ts';
|
|
6
6
|
import { runTextQuery } from './sdk-utils.ts';
|
|
7
|
+
import { GENESIS_HASH, readAuditHead } from './security/audit.ts';
|
|
7
8
|
|
|
8
9
|
const TAG = '[data-sync]';
|
|
9
10
|
const DEPLOYMENT_ID_FILE = '.deployment-id';
|
|
11
|
+
/** Tracked, append-only, single-writer (security/audit.ts): committed on every flush, never pulled over, never stashed. */
|
|
12
|
+
const AUDIT_DIR = 'audit';
|
|
13
|
+
const NOT_AUDIT = ['--', '.', `:(exclude)${AUDIT_DIR}`];
|
|
10
14
|
|
|
11
15
|
/** How long the LLM commit-message call may take before we fall back (ms). */
|
|
12
16
|
const COMMIT_MSG_TIMEOUT_MS = 60_000;
|
|
@@ -357,11 +361,32 @@ export class DataSync {
|
|
|
357
361
|
return;
|
|
358
362
|
}
|
|
359
363
|
|
|
360
|
-
//
|
|
361
|
-
|
|
364
|
+
// The audit log is append-only with ONE writer, this instance. A remote commit touching audit/ would rewrite the local
|
|
365
|
+
// chain (and under `chattr +a` git can't even apply it) — refuse the whole pull and alert.
|
|
366
|
+
let incomingAudit: string;
|
|
367
|
+
try {
|
|
368
|
+
incomingAudit = (await this.git('diff', '--name-only', `HEAD...origin/${this.options.branch}`, '--', AUDIT_DIR)).trim();
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.warn(`${TAG} Audit pull check failed, skipping pull:`, (err as Error).message);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (incomingAudit) {
|
|
374
|
+
console.error(`${TAG} 🚫 Remote commits change the audit log — pull refused:\n${incomingAudit}`);
|
|
375
|
+
await this.alertOnce('audit-pull', incomingAudit,
|
|
376
|
+
`🚫 Data sync refused a pull: remote commits change the audit log, which only this instance writes.\n\n${incomingAudit}\n\n` +
|
|
377
|
+
`Inspect: \`cd data && git log origin/${this.options.branch} -- ${AUDIT_DIR}\``,
|
|
378
|
+
).catch(err => console.warn(`${TAG} Audit pull notify failed:`, (err as Error).message));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
this.clearAlert('audit-pull');
|
|
382
|
+
|
|
383
|
+
// Stash dirty + untracked files before merging (untracked can block merge if remote adds same paths) — except audit/:
|
|
384
|
+
// stashing removes the live log from disk while the server appends to it (lost lines, forked chain) and fails under
|
|
385
|
+
// `chattr +a`. The merge can't touch audit/ (checked above), so it stays dirty in place.
|
|
386
|
+
const dirty = !!(await this.git('status', '--porcelain', ...NOT_AUDIT)).trim();
|
|
362
387
|
if (dirty) {
|
|
363
388
|
try {
|
|
364
|
-
await this.git('stash', 'push', '--include-untracked', '-m', 'data-sync: pre-pull stash');
|
|
389
|
+
await this.git('stash', 'push', '--include-untracked', '-m', 'data-sync: pre-pull stash', ...NOT_AUDIT);
|
|
365
390
|
} catch (err) {
|
|
366
391
|
console.warn(`${TAG} Stash failed, skipping pull:`, (err as Error).message);
|
|
367
392
|
return;
|
|
@@ -441,12 +466,19 @@ export class DataSync {
|
|
|
441
466
|
}
|
|
442
467
|
}
|
|
443
468
|
|
|
469
|
+
// The audit log rides along with every sync commit — the data repo is its offsite copy — and the commit message
|
|
470
|
+
// anchors its head. Head read BEFORE staging: the committed chain always contains that hash.
|
|
471
|
+
const auditHead = this.auditHead();
|
|
472
|
+
if (existsSync(path.join(DATA_DIR, AUDIT_DIR))) {
|
|
473
|
+
await this.git('add', '--', AUDIT_DIR).catch(err => console.warn(`${TAG} Staging audit log failed:`, (err as Error).message));
|
|
474
|
+
}
|
|
475
|
+
|
|
444
476
|
const status = await this.git('status', '--porcelain');
|
|
445
477
|
if (!status.trim()) return;
|
|
446
478
|
|
|
447
479
|
if (await this.guardMassDeletions('flush')) return;
|
|
448
480
|
const msg = await this.generateCommitMessage(files);
|
|
449
|
-
await this.git('commit', '-m', msg).catch(() => {});
|
|
481
|
+
await this.git('commit', '-m', auditHead ? `${msg}\n\naudit-head: ${auditHead}` : msg).catch(() => {});
|
|
450
482
|
const ahead = await this.git('rev-list', '--count', `origin/${this.options.branch}..HEAD`).catch(() => '0');
|
|
451
483
|
if (parseInt(ahead.trim()) === 0) return;
|
|
452
484
|
await this.git('push', 'origin', this.options.branch).catch(async (err) => {
|
|
@@ -468,11 +500,23 @@ export class DataSync {
|
|
|
468
500
|
}
|
|
469
501
|
}
|
|
470
502
|
|
|
503
|
+
/** Audit chain head for the commit anchor; undefined when there's no record yet or it can't be read (logged). */
|
|
504
|
+
private auditHead(): string | undefined {
|
|
505
|
+
try {
|
|
506
|
+
const head = readAuditHead(path.join(DATA_DIR, AUDIT_DIR));
|
|
507
|
+
return head === GENESIS_HASH ? undefined : head;
|
|
508
|
+
} catch (err) {
|
|
509
|
+
console.warn(`${TAG} Audit head unreadable, committing without anchor:`, (err as Error).message);
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
471
514
|
private async generateCommitMessage(files: string[]): Promise<string> {
|
|
472
515
|
const fallback = fallbackCommitMessage(files);
|
|
473
516
|
try {
|
|
474
|
-
|
|
475
|
-
const
|
|
517
|
+
// The audit log's appended lines are not "behavioral config" and would crowd the real diff out of the prompt.
|
|
518
|
+
const diff = await this.git('diff', '--cached', '--stat', ...NOT_AUDIT).catch(() => '');
|
|
519
|
+
const diffContent = await this.git('diff', '--cached', '--no-color', '-U2', ...NOT_AUDIT).catch(() => '');
|
|
476
520
|
if (!diffContent.trim()) return fallback;
|
|
477
521
|
const truncated = diffContent.slice(0, 3000);
|
|
478
522
|
// Bounded: a hung `claude` subprocess used to wedge flush() forever (it holds the push latch).
|
|
@@ -761,7 +805,12 @@ export class DataSync {
|
|
|
761
805
|
'uploads/', 'repos/', '.tmp/', 'schedules.json.bak', 'git-log.json',
|
|
762
806
|
'.internal-token', 'comms-log.jsonl', 'sessions/', 'unread/', '.DS_Store',
|
|
763
807
|
'scheduler/', '.mcp-catalog.json',
|
|
808
|
+
'api-keys.json.bak', // pre-hashing plaintext keys (api-keys.ts migration) — never commit
|
|
764
809
|
'workspace/users/*/.claude/', // per-user Claude logins (claude-account.ts) — credentials, never commit
|
|
810
|
+
// Live security state of the ACTIVE instance, which is its single writer — a pull must never overwrite it:
|
|
811
|
+
// blocks.json (guard auto-blocks); policy.json + .migrated (the Owner Console writes them; a pulled change trips
|
|
812
|
+
// the policy provenance/tamper check). Blue-green shares one DATA_DIR on one host, so nothing needs to cross hosts.
|
|
813
|
+
'security/',
|
|
765
814
|
];
|
|
766
815
|
|
|
767
816
|
/** Write or refresh .gitignore, appending any canonical entries it's missing (idempotent). */
|
package/src/server/directives.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface Directives {
|
|
|
4
4
|
thinking?: 'adaptive' | 'enabled' | 'disabled';
|
|
5
5
|
effort?: 'low' | 'medium' | 'high' | 'max';
|
|
6
6
|
engine?: string;
|
|
7
|
+
/** claude-code SDK session resume for this conversation (overrides agent-config `sdkResume`). */
|
|
8
|
+
resume?: boolean;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export interface ParsedPrompt {
|
|
@@ -16,10 +18,8 @@ export interface ParsedPrompt {
|
|
|
16
18
|
unresolvedModel?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* Opus 4.7), which burns rate limits and budget. */
|
|
22
|
-
export const DEFAULT_MODEL = 'claude-sonnet-5';
|
|
21
|
+
// The default model lives in src/shared/models.ts so the client header names the same one.
|
|
22
|
+
export { DEFAULT_MODEL } from '../shared/models';
|
|
23
23
|
|
|
24
24
|
// Canonical model aliases + label. Vendored, pure, dependency-free (src/server/model-aliases.ts).
|
|
25
25
|
// Re-exported here so the rest of shraga keeps importing model helpers from one place.
|
|
@@ -28,7 +28,7 @@ import { MODEL_ALIASES } from './model-aliases.ts';
|
|
|
28
28
|
|
|
29
29
|
const DIRECTIVE_RE = /^\s*\[([^\]]*)\]\s*([\s\S]*)/;
|
|
30
30
|
|
|
31
|
-
const DIRECTIVE_KEYS = ['model', 'turns', 'thinking', 'think', 'effort', 'engine'];
|
|
31
|
+
const DIRECTIVE_KEYS = ['model', 'turns', 'thinking', 'think', 'effort', 'engine', 'resume'];
|
|
32
32
|
|
|
33
33
|
/** MODEL_ALIASES only covers the bare Anthropic shorthands. A `provider/model` id
|
|
34
34
|
* (`cursor/composer-2.5`, `openai/gpt-5.6`) is already concrete — gating it on the alias table
|
|
@@ -179,6 +179,11 @@ function applyDirective(d: Directives, key: string, val: string): string | undef
|
|
|
179
179
|
case 'engine':
|
|
180
180
|
d.engine = val;
|
|
181
181
|
break;
|
|
182
|
+
case 'resume':
|
|
183
|
+
if (['on', 'true', '1'].includes(val)) d.resume = true;
|
|
184
|
+
else if (['off', 'false', '0'].includes(val)) d.resume = false;
|
|
185
|
+
else console.warn(`[directives] Invalid resume value: "${val}"`);
|
|
186
|
+
break;
|
|
182
187
|
default:
|
|
183
188
|
console.warn(`[directives] Unknown directive key: "${key}"`);
|
|
184
189
|
}
|