@skrr-ai/cli 0.1.125 → 0.1.127
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/dist/commands/agents/chat.d.ts +22 -0
- package/dist/commands/agents/chat.js +71 -15
- package/dist/commands/agents/goals/list.d.ts +2 -2
- package/dist/commands/agents/goals/list.js +2 -2
- package/dist/commands/goals/create.d.ts +1 -0
- package/dist/commands/goals/create.js +4 -1
- package/dist/commands/goals/revisions.js +3 -19
- package/dist/commands/spaces/revisions.js +3 -2
- package/dist/commands/tasks/revisions.js +3 -2
- package/dist/commands/triggers/fire-detail.js +2 -0
- package/dist/lib/revision-actor.d.ts +2 -0
- package/dist/lib/revision-actor.js +16 -0
- package/dist/lib/triggers.d.ts +32 -0
- package/dist/lib/triggers.js +66 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/scheduled-stream-wire.cjs +98 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/scheduled-stream-wire.d.cts +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/package.json +4 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +9323 -9270
- package/oclif.manifest.json +8 -1
- package/package.json +1 -1
|
@@ -25,6 +25,28 @@ export declare const DETACH_EXIT_CODE = 130;
|
|
|
25
25
|
* answers true.
|
|
26
26
|
*/
|
|
27
27
|
export declare const STREAM_LOST_EXIT_CODE = 75;
|
|
28
|
+
/**
|
|
29
|
+
* The signals that stop a running chat.
|
|
30
|
+
*
|
|
31
|
+
* Ctrl-C was the only one handled (OSK-2938). SIGTERM (a CI cancel, `timeout`, a process manager) and
|
|
32
|
+
* SIGHUP (the terminal closing) killed the CLI in under 100 ms with no cancel sent, so the agent kept
|
|
33
|
+
* running tools on the person's machine with nobody watching and held the folder, and the next message
|
|
34
|
+
* was refused as `managed_task_workspace_busy` (OSK-20677). `machines dedicated cp` already treats all
|
|
35
|
+
* three as cancel and exits as a shell would; this does the same.
|
|
36
|
+
*/
|
|
37
|
+
export declare const INTERRUPT_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
38
|
+
export type InterruptSignal = (typeof INTERRUPT_SIGNALS)[number];
|
|
39
|
+
/** 128 plus the signal's number, as a shell reports a process the signal killed: 130, 143, 129. */
|
|
40
|
+
export declare function interruptExitCode(signal: InterruptSignal | null): number;
|
|
41
|
+
/**
|
|
42
|
+
* The cancel notice, in words that fit the signal. "Press Ctrl-C again" is wrong advice after a
|
|
43
|
+
* SIGTERM, and after SIGHUP nobody is looking: the terminal that would show it is gone.
|
|
44
|
+
*/
|
|
45
|
+
export declare function cancelNotice(input: {
|
|
46
|
+
signal: InterruptSignal;
|
|
47
|
+
sessionId: string;
|
|
48
|
+
graceSeconds: number;
|
|
49
|
+
}): string | null;
|
|
28
50
|
/**
|
|
29
51
|
* Exit code for a streamed run that reached its terminal state: 1 when it
|
|
30
52
|
* failed, 0 otherwise.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MAX_HANDOVER_WAIT_SECONDS = exports.DEFAULT_HANDOVER_WAIT_SECONDS = exports.STREAM_LOST_EXIT_CODE = exports.DETACH_EXIT_CODE = void 0;
|
|
3
|
+
exports.MAX_HANDOVER_WAIT_SECONDS = exports.DEFAULT_HANDOVER_WAIT_SECONDS = exports.INTERRUPT_SIGNALS = exports.STREAM_LOST_EXIT_CODE = exports.DETACH_EXIT_CODE = void 0;
|
|
4
|
+
exports.interruptExitCode = interruptExitCode;
|
|
5
|
+
exports.cancelNotice = cancelNotice;
|
|
4
6
|
exports.streamedResultExitCode = streamedResultExitCode;
|
|
5
7
|
exports.interruptedDetachMessage = interruptedDetachMessage;
|
|
6
8
|
exports.jsonlTextForm = jsonlTextForm;
|
|
@@ -49,6 +51,35 @@ exports.DETACH_EXIT_CODE = 130;
|
|
|
49
51
|
* answers true.
|
|
50
52
|
*/
|
|
51
53
|
exports.STREAM_LOST_EXIT_CODE = 75;
|
|
54
|
+
/**
|
|
55
|
+
* The signals that stop a running chat.
|
|
56
|
+
*
|
|
57
|
+
* Ctrl-C was the only one handled (OSK-2938). SIGTERM (a CI cancel, `timeout`, a process manager) and
|
|
58
|
+
* SIGHUP (the terminal closing) killed the CLI in under 100 ms with no cancel sent, so the agent kept
|
|
59
|
+
* running tools on the person's machine with nobody watching and held the folder, and the next message
|
|
60
|
+
* was refused as `managed_task_workspace_busy` (OSK-20677). `machines dedicated cp` already treats all
|
|
61
|
+
* three as cancel and exits as a shell would; this does the same.
|
|
62
|
+
*/
|
|
63
|
+
exports.INTERRUPT_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
64
|
+
/** 128 plus the signal's number, as a shell reports a process the signal killed: 130, 143, 129. */
|
|
65
|
+
function interruptExitCode(signal) {
|
|
66
|
+
if (signal === 'SIGTERM')
|
|
67
|
+
return 143;
|
|
68
|
+
if (signal === 'SIGHUP')
|
|
69
|
+
return 129;
|
|
70
|
+
return exports.DETACH_EXIT_CODE;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The cancel notice, in words that fit the signal. "Press Ctrl-C again" is wrong advice after a
|
|
74
|
+
* SIGTERM, and after SIGHUP nobody is looking: the terminal that would show it is gone.
|
|
75
|
+
*/
|
|
76
|
+
function cancelNotice(input) {
|
|
77
|
+
if (input.signal === 'SIGHUP')
|
|
78
|
+
return null;
|
|
79
|
+
const again = input.signal === 'SIGINT' ? 'press Ctrl-C again' : `send ${input.signal} again`;
|
|
80
|
+
return (`\nCancelling run ${input.sessionId}… (waiting up to ${input.graceSeconds}s; ${again} to detach). ` +
|
|
81
|
+
'A tool call already running on the runtime is not guaranteed to stop.');
|
|
82
|
+
}
|
|
52
83
|
/**
|
|
53
84
|
* Exit code for a streamed run that reached its terminal state: 1 when it
|
|
54
85
|
* failed, 0 otherwise.
|
|
@@ -882,10 +913,22 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
882
913
|
detach = () => resolve('detached');
|
|
883
914
|
});
|
|
884
915
|
let interrupts = 0;
|
|
916
|
+
// The signal that started the stop. Its exit status is what a script reads (130, 143 or 129).
|
|
917
|
+
let interruptedBy = null;
|
|
918
|
+
// After SIGHUP the terminal is gone and a write to it throws; the notice is lost, the stop is not.
|
|
919
|
+
const writeNotice = (fd, text) => {
|
|
920
|
+
try {
|
|
921
|
+
(0, node_fs_1.writeSync)(fd, text);
|
|
922
|
+
}
|
|
923
|
+
catch (_error) {
|
|
924
|
+
/* the terminal that would show this is closed */
|
|
925
|
+
}
|
|
926
|
+
};
|
|
885
927
|
let forcedDetachAnnounced = false;
|
|
886
928
|
let graceTimer = null;
|
|
887
|
-
const onInterrupt = () => {
|
|
929
|
+
const onInterrupt = (signal = 'SIGINT') => {
|
|
888
930
|
interrupts += 1;
|
|
931
|
+
interruptedBy ??= signal;
|
|
889
932
|
const action = interruptAction({ interrupts, awaitingHandover: client.awaitingHandover() });
|
|
890
933
|
if (action === 'stop-handover-wait') {
|
|
891
934
|
// Nothing to cancel and nothing to grace-wait for: stop at once.
|
|
@@ -896,7 +939,7 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
896
939
|
conversationId: started.conversationId,
|
|
897
940
|
});
|
|
898
941
|
if (flags.jsonl) {
|
|
899
|
-
(
|
|
942
|
+
writeNotice(process.stdout.fd, `${JSON.stringify({
|
|
900
943
|
type: 'run.detached',
|
|
901
944
|
sessionId: started.sessionId,
|
|
902
945
|
conversationId: started.conversationId,
|
|
@@ -906,9 +949,9 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
906
949
|
})}\n`);
|
|
907
950
|
}
|
|
908
951
|
else {
|
|
909
|
-
(
|
|
952
|
+
writeNotice(process.stderr.fd, `\n${message}\n`);
|
|
910
953
|
}
|
|
911
|
-
process.exitCode =
|
|
954
|
+
process.exitCode = interruptExitCode(interruptedBy);
|
|
912
955
|
}
|
|
913
956
|
client.close();
|
|
914
957
|
detach?.();
|
|
@@ -917,9 +960,13 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
917
960
|
if (action === 'cancel') {
|
|
918
961
|
client.cancel();
|
|
919
962
|
if (!flags.jsonl) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
963
|
+
const notice = cancelNotice({
|
|
964
|
+
signal,
|
|
965
|
+
sessionId: started.sessionId,
|
|
966
|
+
graceSeconds: CANCEL_GRACE_MS / 1000,
|
|
967
|
+
});
|
|
968
|
+
if (notice)
|
|
969
|
+
writeNotice(process.stdout.fd, `${notice}\n`);
|
|
923
970
|
}
|
|
924
971
|
graceTimer = setTimeout(() => detach?.(), CANCEL_GRACE_MS);
|
|
925
972
|
graceTimer.unref?.();
|
|
@@ -933,7 +980,7 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
933
980
|
forced: true,
|
|
934
981
|
});
|
|
935
982
|
if (flags.jsonl) {
|
|
936
|
-
(
|
|
983
|
+
writeNotice(process.stdout.fd, `${JSON.stringify({
|
|
937
984
|
type: 'run.detached',
|
|
938
985
|
sessionId: started.sessionId,
|
|
939
986
|
conversationId: started.conversationId,
|
|
@@ -942,13 +989,14 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
942
989
|
})}\n`);
|
|
943
990
|
}
|
|
944
991
|
else {
|
|
945
|
-
(
|
|
992
|
+
writeNotice(process.stderr.fd, `\n${message}\n`);
|
|
946
993
|
}
|
|
947
|
-
process.exitCode =
|
|
994
|
+
process.exitCode = interruptExitCode(interruptedBy);
|
|
948
995
|
}
|
|
949
996
|
detach?.();
|
|
950
997
|
};
|
|
951
|
-
|
|
998
|
+
for (const name of exports.INTERRUPT_SIGNALS)
|
|
999
|
+
process.on(name, onInterrupt);
|
|
952
1000
|
try {
|
|
953
1001
|
// OSK-2944 — the run was ACCEPTED before we ever tried to attach. If the
|
|
954
1002
|
// stream cannot be attached, that is a lost view of a live run, not a
|
|
@@ -988,7 +1036,7 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
988
1036
|
// 130 only when a signal actually caused this; otherwise the stream was
|
|
989
1037
|
// lost and claiming SIGINT lies to every script reading the code
|
|
990
1038
|
// (OSK-7660).
|
|
991
|
-
const exit = interrupts > 0 ?
|
|
1039
|
+
const exit = interrupts > 0 ? interruptExitCode(interruptedBy) : exports.STREAM_LOST_EXIT_CODE;
|
|
992
1040
|
if (flags.jsonl) {
|
|
993
1041
|
(0, agentic_stream_1.writeJsonLine)(process.stdout, {
|
|
994
1042
|
type: 'run.detached',
|
|
@@ -1039,15 +1087,23 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
1039
1087
|
this.error(noOutput, { exit: 1 });
|
|
1040
1088
|
this.log(result.text);
|
|
1041
1089
|
}
|
|
1042
|
-
else if (result.status === 'aborted') {
|
|
1090
|
+
else if (result.status === 'aborted' && interruptedBy !== 'SIGHUP') {
|
|
1043
1091
|
// Streaming human mode: say the cancel took, rather than leave the keypress warning last.
|
|
1092
|
+
// (Not after SIGHUP: the terminal that would show it is gone.)
|
|
1044
1093
|
this.log(`\n${cancelledRunMessage({ sessionId: started.sessionId, conversationId: started.conversationId })}`);
|
|
1045
1094
|
}
|
|
1095
|
+
// A run stopped by SIGTERM or SIGHUP ends the way a shell reports a process the signal killed,
|
|
1096
|
+
// so a script can tell a cancelled run from one that finished. Ctrl-C keeps its existing status
|
|
1097
|
+
// (a cancel that took is a success, a detach is 130).
|
|
1098
|
+
if (interruptedBy === 'SIGTERM' || interruptedBy === 'SIGHUP') {
|
|
1099
|
+
process.exitCode = interruptExitCode(interruptedBy);
|
|
1100
|
+
}
|
|
1046
1101
|
}
|
|
1047
1102
|
finally {
|
|
1048
1103
|
if (graceTimer)
|
|
1049
1104
|
clearTimeout(graceTimer);
|
|
1050
|
-
|
|
1105
|
+
for (const name of exports.INTERRUPT_SIGNALS)
|
|
1106
|
+
process.removeListener(name, onInterrupt);
|
|
1051
1107
|
client.close();
|
|
1052
1108
|
}
|
|
1053
1109
|
}
|
|
@@ -8,8 +8,8 @@ import { BaseCommand } from '../../../base-command';
|
|
|
8
8
|
* Wraps `GET /api/agents/:agent_id/goals` (data-provider `listAgentGoals`),
|
|
9
9
|
* which returns goals where the agent is owner, DRI, or a contributor, each
|
|
10
10
|
* enriched with `_agentGoalRole` (which of the three) and `_completionPercent`.
|
|
11
|
-
* The endpoint sorts by most-recently-updated and
|
|
12
|
-
*
|
|
11
|
+
* The endpoint sorts by most-recently-updated and supports cursor pagination.
|
|
12
|
+
* This command currently shows its first page of up to 100 goals.
|
|
13
13
|
*/
|
|
14
14
|
export default class AgentsGoalsList extends BaseCommand {
|
|
15
15
|
static description: string;
|
|
@@ -14,8 +14,8 @@ const format_1 = require("../../../lib/format");
|
|
|
14
14
|
* Wraps `GET /api/agents/:agent_id/goals` (data-provider `listAgentGoals`),
|
|
15
15
|
* which returns goals where the agent is owner, DRI, or a contributor, each
|
|
16
16
|
* enriched with `_agentGoalRole` (which of the three) and `_completionPercent`.
|
|
17
|
-
* The endpoint sorts by most-recently-updated and
|
|
18
|
-
*
|
|
17
|
+
* The endpoint sorts by most-recently-updated and supports cursor pagination.
|
|
18
|
+
* This command currently shows its first page of up to 100 goals.
|
|
19
19
|
*/
|
|
20
20
|
class AgentsGoalsList extends base_command_1.BaseCommand {
|
|
21
21
|
static description = 'List the goals an agent owns, is DRI for, or contributes to';
|
|
@@ -6,6 +6,7 @@ export default class GoalsCreate extends BaseCommand {
|
|
|
6
6
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
'from-json': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
level: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
'target-date': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -24,6 +24,9 @@ class GoalsCreate extends base_command_1.BaseCommand {
|
|
|
24
24
|
description: 'Path to a JSON file (or "-" for stdin) to use as the request body',
|
|
25
25
|
}),
|
|
26
26
|
title: core_1.Flags.string({ description: 'Goal title (required unless set via --from-json)' }),
|
|
27
|
+
'idempotency-key': core_1.Flags.string({
|
|
28
|
+
description: 'Stable key for retries of this exact Goal creation request',
|
|
29
|
+
}),
|
|
27
30
|
description: core_1.Flags.string({ description: 'Goal description' }),
|
|
28
31
|
level: core_1.Flags.string({
|
|
29
32
|
description: 'Goal level',
|
|
@@ -126,7 +129,7 @@ class GoalsCreate extends base_command_1.BaseCommand {
|
|
|
126
129
|
payload.labelIds = resolvedLabelIds;
|
|
127
130
|
let created;
|
|
128
131
|
try {
|
|
129
|
-
created = await data_provider_1.dataService.createGoal(payload);
|
|
132
|
+
created = await data_provider_1.dataService.createGoal(payload, flags['idempotency-key'] ? { idempotencyKey: flags['idempotency-key'] } : undefined);
|
|
130
133
|
if (flags.space) {
|
|
131
134
|
const goalId = created.id;
|
|
132
135
|
if (!goalId || typeof goalId !== 'string') {
|
|
@@ -4,20 +4,7 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
5
5
|
const base_command_1 = require("../../base-command");
|
|
6
6
|
const format_1 = require("../../lib/format");
|
|
7
|
-
|
|
8
|
-
* The one class of goal change this log does not contain.
|
|
9
|
-
*
|
|
10
|
-
* `PATCH /api/goals/:goalId/planner-config` writes the goal through
|
|
11
|
-
* `Goal.findOneAndUpdate` directly rather than `GoalModel.updateGoal`, which is
|
|
12
|
-
* where `RevisionService` is called from — so arming, re-timing or disarming the
|
|
13
|
-
* auto-planner (and any `maxActiveTasks` change made through that route) leaves
|
|
14
|
-
* no revision. "Who turned the planner on for this goal, and when" is the audit
|
|
15
|
-
* question a shared goal actually raises, and the table answers it with silence.
|
|
16
|
-
*
|
|
17
|
-
* Naming the gap is the honest thing the CLI can do; closing it is a server
|
|
18
|
-
* change. DELETE THIS FOOTER once the planner-config route records a revision.
|
|
19
|
-
*/
|
|
20
|
-
const PLANNER_GAP_NOTE = 'Note: auto-planner changes made with `goals planner-config` are not recorded as revisions.';
|
|
7
|
+
const revision_actor_1 = require("../../lib/revision-actor");
|
|
21
8
|
class GoalsRevisions extends base_command_1.BaseCommand {
|
|
22
9
|
static description = 'List revisions for a goal';
|
|
23
10
|
static examples = [
|
|
@@ -47,25 +34,22 @@ class GoalsRevisions extends base_command_1.BaseCommand {
|
|
|
47
34
|
const items = result?.data ?? [];
|
|
48
35
|
if (items.length === 0) {
|
|
49
36
|
this.log('No revisions.');
|
|
50
|
-
this.log(PLANNER_GAP_NOTE);
|
|
51
37
|
return;
|
|
52
38
|
}
|
|
53
39
|
const rows = items.map((rev) => ({
|
|
54
40
|
version: String(rev.revisionNumber ?? '-'),
|
|
55
41
|
changeType: String(rev.changeType ?? '-'),
|
|
56
|
-
user:
|
|
42
|
+
user: (0, revision_actor_1.revisionActorLabel)(rev),
|
|
57
43
|
changes: String(Array.isArray(rev.changes) ? rev.changes.length : 0),
|
|
58
44
|
when: rev.createdAt ? String(rev.createdAt).slice(0, 19).replace('T', ' ') : '-',
|
|
59
45
|
}));
|
|
60
46
|
(0, format_1.renderTable)(rows, [
|
|
61
47
|
{ key: 'version', header: 'REV' },
|
|
62
48
|
{ key: 'changeType', header: 'CHANGE' },
|
|
63
|
-
{ key: 'user', header: '
|
|
49
|
+
{ key: 'user', header: 'ACTOR' },
|
|
64
50
|
{ key: 'changes', header: 'FIELDS' },
|
|
65
51
|
{ key: 'when', header: 'AT' },
|
|
66
52
|
], (line) => this.log(line));
|
|
67
|
-
this.log('');
|
|
68
|
-
this.log(PLANNER_GAP_NOTE);
|
|
69
53
|
}
|
|
70
54
|
}
|
|
71
55
|
exports.default = GoalsRevisions;
|
|
@@ -5,6 +5,7 @@ const data_provider_1 = require("@skrr-ai/data-provider");
|
|
|
5
5
|
const base_command_1 = require("../../base-command");
|
|
6
6
|
const format_1 = require("../../lib/format");
|
|
7
7
|
const space_output_1 = require("../../lib/space-output");
|
|
8
|
+
const revision_actor_1 = require("../../lib/revision-actor");
|
|
8
9
|
class SpacesRevisions extends base_command_1.BaseCommand {
|
|
9
10
|
static description = 'List revisions for a space';
|
|
10
11
|
static examples = [
|
|
@@ -40,14 +41,14 @@ class SpacesRevisions extends base_command_1.BaseCommand {
|
|
|
40
41
|
const rows = items.map((rev) => ({
|
|
41
42
|
version: String(rev.revisionNumber ?? '-'),
|
|
42
43
|
changeType: String(rev.changeType ?? '-'),
|
|
43
|
-
user:
|
|
44
|
+
user: (0, revision_actor_1.revisionActorLabel)(rev),
|
|
44
45
|
changes: String(Array.isArray(rev.changes) ? rev.changes.length : 0),
|
|
45
46
|
when: rev.createdAt ? String(rev.createdAt).slice(0, 19).replace('T', ' ') : '-',
|
|
46
47
|
}));
|
|
47
48
|
(0, format_1.renderTable)(rows, [
|
|
48
49
|
{ key: 'version', header: 'REV' },
|
|
49
50
|
{ key: 'changeType', header: 'CHANGE' },
|
|
50
|
-
{ key: 'user', header: '
|
|
51
|
+
{ key: 'user', header: 'ACTOR' },
|
|
51
52
|
{ key: 'changes', header: 'FIELDS' },
|
|
52
53
|
{ key: 'when', header: 'AT' },
|
|
53
54
|
], (line) => this.log(line));
|
|
@@ -5,6 +5,7 @@ const core_1 = require("@oclif/core");
|
|
|
5
5
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
6
6
|
const base_command_1 = require("../../base-command");
|
|
7
7
|
const format_1 = require("../../lib/format");
|
|
8
|
+
const revision_actor_1 = require("../../lib/revision-actor");
|
|
8
9
|
/** " (via CLI credential)" when the revision recorded its credential channel. */
|
|
9
10
|
function revisionChannelSuffix(rev) {
|
|
10
11
|
const via = rev.metadata?.via;
|
|
@@ -46,14 +47,14 @@ class TasksRevisions extends base_command_1.BaseCommand {
|
|
|
46
47
|
version: String(rev.revisionNumber ?? '-'),
|
|
47
48
|
changeType: String(rev.changeType ?? '-'),
|
|
48
49
|
// The channel the person's credential came through, when recorded (OSK-16621).
|
|
49
|
-
user: `${
|
|
50
|
+
user: `${(0, revision_actor_1.revisionActorLabel)(rev)}${rev.userId != null ? revisionChannelSuffix(rev) : ''}`,
|
|
50
51
|
changes: String(Array.isArray(rev.changes) ? rev.changes.length : 0),
|
|
51
52
|
when: rev.createdAt ? String(rev.createdAt).slice(0, 19).replace('T', ' ') : '-',
|
|
52
53
|
}));
|
|
53
54
|
(0, format_1.renderTable)(rows, [
|
|
54
55
|
{ key: 'version', header: 'REV' },
|
|
55
56
|
{ key: 'changeType', header: 'CHANGE' },
|
|
56
|
-
{ key: 'user', header: '
|
|
57
|
+
{ key: 'user', header: 'ACTOR' },
|
|
57
58
|
{ key: 'changes', header: 'FIELDS' },
|
|
58
59
|
{ key: 'when', header: 'AT' },
|
|
59
60
|
], (line) => this.log(line));
|
|
@@ -29,6 +29,8 @@ function describeFireAuditDetail(detail, fireId, triggerId) {
|
|
|
29
29
|
lines.push(`Duration: ${String(detail.durationMs)}ms`);
|
|
30
30
|
if (detail.conversationId)
|
|
31
31
|
lines.push(`Conversation: ${String(detail.conversationId)}`);
|
|
32
|
+
// Tool calls refused during a turn that was delivered: `triggers show` points here for the facts.
|
|
33
|
+
lines.push(...(0, triggers_1.fireRefusalLines)(detail.metadata));
|
|
32
34
|
return lines;
|
|
33
35
|
}
|
|
34
36
|
class TriggersFireDetail extends base_command_1.BaseCommand {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.revisionActorLabel = revisionActorLabel;
|
|
4
|
+
/** Render the actual actor without presenting a system or Agent as a user. */
|
|
5
|
+
function revisionActorLabel(revision) {
|
|
6
|
+
if (revision.userId != null)
|
|
7
|
+
return String(revision.userId);
|
|
8
|
+
const actor = revision.actor;
|
|
9
|
+
if (actor && typeof actor === 'object' && !Array.isArray(actor)) {
|
|
10
|
+
const { kind, id } = actor;
|
|
11
|
+
if ((kind === 'system' || kind === 'agent') && typeof id === 'string' && id) {
|
|
12
|
+
return `${kind}: ${id}`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return '-';
|
|
16
|
+
}
|
package/dist/lib/triggers.d.ts
CHANGED
|
@@ -158,6 +158,15 @@ export interface TriggerFireRecord {
|
|
|
158
158
|
replayBlockedReason?: string;
|
|
159
159
|
replayRequirements?: string[];
|
|
160
160
|
replayContext?: unknown;
|
|
161
|
+
/** Tool calls refused during a turn that was delivered (OSK-20776). */
|
|
162
|
+
permissionRefusals?: {
|
|
163
|
+
count?: number;
|
|
164
|
+
tools?: string[];
|
|
165
|
+
unattended?: boolean;
|
|
166
|
+
layer?: string;
|
|
167
|
+
};
|
|
168
|
+
/** The refusal record could not be read for this fire, which is not "none were refused". */
|
|
169
|
+
permissionRefusalsUnread?: boolean;
|
|
161
170
|
};
|
|
162
171
|
}
|
|
163
172
|
export interface TriggerListResponse {
|
|
@@ -516,6 +525,29 @@ export declare function writeModeLabel(writeMode: string): string;
|
|
|
516
525
|
* the configured base URL rather than guessed.
|
|
517
526
|
*/
|
|
518
527
|
export declare function webhookIngestUrl(baseURL: string, triggerId: string): string;
|
|
528
|
+
/**
|
|
529
|
+
* What `triggers show` adds when the last fire DELIVERED but had tool calls
|
|
530
|
+
* refused (OSK-20776). The outcome of such a fire is `succeeded`, so the
|
|
531
|
+
* `Fires:` line reads as success; this says what the string after `last` means
|
|
532
|
+
* and what a person can do about it.
|
|
533
|
+
*
|
|
534
|
+
* It states the mechanism and not this fire's cause, because `lastResult` names
|
|
535
|
+
* the refused kinds only. Whether a fire was unattended, and which layer
|
|
536
|
+
* refused, is on the fire record (`fire-detail`), and this points there.
|
|
537
|
+
*
|
|
538
|
+
* It does not recommend a mode. `bypassPermissions` is the only mode under which
|
|
539
|
+
* an unattended shell call runs, and it runs EVERY call without asking, which is
|
|
540
|
+
* a decision about what this agent may do on its own (a release job included),
|
|
541
|
+
* not a hint to paste.
|
|
542
|
+
*/
|
|
543
|
+
export declare function toolsRefusedHintLines(trigger: TriggerDefinition, kinds: readonly string[], bin?: string): string[];
|
|
544
|
+
/**
|
|
545
|
+
* The refusal facts a fire record holds, as `fire-detail` lines. Nothing when
|
|
546
|
+
* the fire recorded none. A fire whose record could not be read says so: that
|
|
547
|
+
* is not the same as no refusal, and a reader must not be able to mistake it
|
|
548
|
+
* for one.
|
|
549
|
+
*/
|
|
550
|
+
export declare function fireRefusalLines(metadata: unknown): string[];
|
|
519
551
|
export declare function renderTriggerSummary(trigger: TriggerDefinition, log: (line: string) => void, options?: {
|
|
520
552
|
baseURL?: string;
|
|
521
553
|
}): void;
|
package/dist/lib/triggers.js
CHANGED
|
@@ -48,6 +48,8 @@ exports.formatDuplicateSubscriptionWarning = formatDuplicateSubscriptionWarning;
|
|
|
48
48
|
exports.armedStateLabel = armedStateLabel;
|
|
49
49
|
exports.writeModeLabel = writeModeLabel;
|
|
50
50
|
exports.webhookIngestUrl = webhookIngestUrl;
|
|
51
|
+
exports.toolsRefusedHintLines = toolsRefusedHintLines;
|
|
52
|
+
exports.fireRefusalLines = fireRefusalLines;
|
|
51
53
|
exports.renderTriggerSummary = renderTriggerSummary;
|
|
52
54
|
exports.renderLegacyRuns = renderLegacyRuns;
|
|
53
55
|
exports.renderFireAudit = renderFireAudit;
|
|
@@ -1276,6 +1278,65 @@ const WEBHOOK_INGEST_KINDS = new Set(['on_demand', 'form', 'chat_message']);
|
|
|
1276
1278
|
function webhookIngestUrl(baseURL, triggerId) {
|
|
1277
1279
|
return `${String(baseURL).replace(/\/+$/, '')}/api/triggers/webhook/${encodeURIComponent(triggerId)}`;
|
|
1278
1280
|
}
|
|
1281
|
+
/** An id or token that is short and plain, so it is safe in a command a person will paste. */
|
|
1282
|
+
const PLAIN_TOKEN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1283
|
+
/**
|
|
1284
|
+
* What `triggers show` adds when the last fire DELIVERED but had tool calls
|
|
1285
|
+
* refused (OSK-20776). The outcome of such a fire is `succeeded`, so the
|
|
1286
|
+
* `Fires:` line reads as success; this says what the string after `last` means
|
|
1287
|
+
* and what a person can do about it.
|
|
1288
|
+
*
|
|
1289
|
+
* It states the mechanism and not this fire's cause, because `lastResult` names
|
|
1290
|
+
* the refused kinds only. Whether a fire was unattended, and which layer
|
|
1291
|
+
* refused, is on the fire record (`fire-detail`), and this points there.
|
|
1292
|
+
*
|
|
1293
|
+
* It does not recommend a mode. `bypassPermissions` is the only mode under which
|
|
1294
|
+
* an unattended shell call runs, and it runs EVERY call without asking, which is
|
|
1295
|
+
* a decision about what this agent may do on its own (a release job included),
|
|
1296
|
+
* not a hint to paste.
|
|
1297
|
+
*/
|
|
1298
|
+
function toolsRefusedHintLines(trigger, kinds, bin = 'skrr') {
|
|
1299
|
+
const scopedAgent = trigger.scope?.kind === 'agent' ? trigger.scope.refs?.agentId : undefined;
|
|
1300
|
+
const agent = typeof scopedAgent === 'string' && PLAIN_TOKEN.test(scopedAgent) ? scopedAgent : '<agent>';
|
|
1301
|
+
const triggerId = typeof trigger.id === 'string' && PLAIN_TOKEN.test(trigger.id) ? trigger.id : '<trigger-id>';
|
|
1302
|
+
const what = kinds.length > 0 ? kinds.join(', ') : 'some tool calls';
|
|
1303
|
+
return [
|
|
1304
|
+
`Refused: ${what} - the last fire delivered, but these tool calls were refused`,
|
|
1305
|
+
' A scheduled fire runs unattended: a call that would need approval is refused, because no one is there to give it.',
|
|
1306
|
+
" To allow it, change the agent's permission mode (bypassPermissions runs every tool call without asking, so decide that on purpose):",
|
|
1307
|
+
` ${bin} agents update ${agent} --default-permission-mode <mode> (modes: ${bin} agents update --help)`,
|
|
1308
|
+
` What was refused, per fire: ${bin} triggers audit ${triggerId}, then ${bin} triggers fire-detail ${triggerId} <fire-id>`,
|
|
1309
|
+
];
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* The refusal facts a fire record holds, as `fire-detail` lines. Nothing when
|
|
1313
|
+
* the fire recorded none. A fire whose record could not be read says so: that
|
|
1314
|
+
* is not the same as no refusal, and a reader must not be able to mistake it
|
|
1315
|
+
* for one.
|
|
1316
|
+
*/
|
|
1317
|
+
function fireRefusalLines(metadata) {
|
|
1318
|
+
if (!metadata || typeof metadata !== 'object')
|
|
1319
|
+
return [];
|
|
1320
|
+
const { permissionRefusals, permissionRefusalsUnread } = metadata;
|
|
1321
|
+
const lines = [];
|
|
1322
|
+
if (permissionRefusals && typeof permissionRefusals === 'object') {
|
|
1323
|
+
const { count, tools, unattended, layer } = permissionRefusals;
|
|
1324
|
+
const kinds = (Array.isArray(tools) ? tools : []).filter((tool) => typeof tool === 'string' && PLAIN_TOKEN.test(tool));
|
|
1325
|
+
const calls = typeof count === 'number' && Number.isFinite(count) && count > 0
|
|
1326
|
+
? `${count} tool call${count === 1 ? '' : 's'}`
|
|
1327
|
+
: 'tool calls';
|
|
1328
|
+
const parts = [`${calls} refused${kinds.length > 0 ? ` (${kinds.join(', ')})` : ''}`];
|
|
1329
|
+
if (unattended === true)
|
|
1330
|
+
parts.push('unattended, so no one could grant them');
|
|
1331
|
+
if (typeof layer === 'string' && PLAIN_TOKEN.test(layer))
|
|
1332
|
+
parts.push(`refused by ${layer}`);
|
|
1333
|
+
lines.push(`Refused: ${parts.join('; ')}`);
|
|
1334
|
+
}
|
|
1335
|
+
if (permissionRefusalsUnread === true) {
|
|
1336
|
+
lines.push('Refused: unknown - the refusal record could not be read for this fire; that is not the same as none');
|
|
1337
|
+
}
|
|
1338
|
+
return lines;
|
|
1339
|
+
}
|
|
1279
1340
|
function renderTriggerSummary(trigger, log, options = {}) {
|
|
1280
1341
|
log(`ID: ${trigger.id ?? '-'}`);
|
|
1281
1342
|
log(`Kind: ${trigger.kind ?? '-'}`);
|
|
@@ -1389,6 +1450,11 @@ function renderTriggerSummary(trigger, log, options = {}) {
|
|
|
1389
1450
|
parts.push(`last ${audit.lastResult}`);
|
|
1390
1451
|
log(`Fires: ${parts.join(', ')}`);
|
|
1391
1452
|
}
|
|
1453
|
+
// `last tools_refused:...` above is a success that held a refusal. Say what that means.
|
|
1454
|
+
const refused = (0, data_provider_1.parseToolsRefusedResult)(audit.lastResult);
|
|
1455
|
+
if (refused)
|
|
1456
|
+
for (const line of toolsRefusedHintLines(trigger, refused))
|
|
1457
|
+
log(line);
|
|
1392
1458
|
// The reason, at the surface a reader reaches for — not only in `--json`.
|
|
1393
1459
|
if (audit.lastError)
|
|
1394
1460
|
log(`Last error: ${audit.lastError}`);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Content-addressed source-envelope manifest. A manifest is completeness
|
|
4
|
+
// evidence only after an authenticated native response binds it to its exact
|
|
5
|
+
// dispatch, and every listed envelope has been independently retained.
|
|
6
|
+
const { createHash } = require('node:crypto');
|
|
7
|
+
const MAX_EVENTS = 1024;
|
|
8
|
+
const MAX_EVENT_BYTES = 256 * 1024;
|
|
9
|
+
const MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
const HASH = /^[0-9a-f]{64}$/;
|
|
11
|
+
const id = value => typeof value === 'string' && value.length > 0 && value.length <= 240;
|
|
12
|
+
const digest = value => createHash('sha256').update(value, 'utf8').digest('hex');
|
|
13
|
+
|
|
14
|
+
function sorted(value) {
|
|
15
|
+
if (Array.isArray(value)) return value.map(sorted);
|
|
16
|
+
if (value && typeof value === 'object') return Object.fromEntries(
|
|
17
|
+
Object.keys(value).sort().map(key => [key, sorted(value[key])]));
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function normalize(envelope) {
|
|
21
|
+
// JSON round-trip defines the wire representation, rejects cycles, and
|
|
22
|
+
// removes undefined properties in the same way as the uploader.
|
|
23
|
+
const value = JSON.parse(JSON.stringify(envelope));
|
|
24
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
25
|
+
!id(value.id) || !value.ev || typeof value.ev !== 'object' || Array.isArray(value.ev))
|
|
26
|
+
throw new TypeError('Invalid scheduled stream envelope');
|
|
27
|
+
// These fields are owned by the server or are independently authenticated
|
|
28
|
+
// native receipts, not part of the replayable provider stream.
|
|
29
|
+
for (const key of ['__seq', 'userSeq', '__degraded', 'messageId', 'managedEventType']) delete value[key];
|
|
30
|
+
for (const key of ['autonomousSourceExecutorTerminal', 'autonomousSourceExecutorNotStarted',
|
|
31
|
+
'ordinaryExecutorTerminal', 'personalContextInvocationTerminal', 'personalContextInvocationNotStarted'])
|
|
32
|
+
delete value.ev[key];
|
|
33
|
+
return JSON.stringify(sorted(value));
|
|
34
|
+
}
|
|
35
|
+
function fingerprint(envelope) {
|
|
36
|
+
const json = normalize(envelope);
|
|
37
|
+
const bytes = Buffer.byteLength(json, 'utf8');
|
|
38
|
+
if (bytes > MAX_EVENT_BYTES) throw new RangeError('Scheduled stream envelope exceeds the limit');
|
|
39
|
+
return { hash: digest(json), bytes, json };
|
|
40
|
+
}
|
|
41
|
+
function create(binding) {
|
|
42
|
+
if (!binding || ![binding.runId, binding.sessionId, binding.requestId, binding.dispatchId].every(id))
|
|
43
|
+
throw new TypeError('Invalid scheduled stream binding');
|
|
44
|
+
let closed = false;
|
|
45
|
+
let unavailable = null;
|
|
46
|
+
let totalBytes = 0;
|
|
47
|
+
const hashes = [];
|
|
48
|
+
return {
|
|
49
|
+
add(envelopes) {
|
|
50
|
+
if (closed) throw new Error('Scheduled stream manifest is already closed');
|
|
51
|
+
if (unavailable || !Array.isArray(envelopes)) return;
|
|
52
|
+
for (const envelope of envelopes) {
|
|
53
|
+
if (hashes.length >= MAX_EVENTS) { unavailable = 'event_limit'; break; }
|
|
54
|
+
let entry;
|
|
55
|
+
try { entry = fingerprint(envelope); }
|
|
56
|
+
catch { unavailable = 'invalid_or_oversize_event'; break; }
|
|
57
|
+
if (totalBytes + entry.bytes > MAX_TOTAL_BYTES) { unavailable = 'byte_limit'; break; }
|
|
58
|
+
hashes.push(entry.hash);
|
|
59
|
+
totalBytes += entry.bytes;
|
|
60
|
+
}
|
|
61
|
+
// A partial prefix is never a manifest.
|
|
62
|
+
if (unavailable) { hashes.length = 0; totalBytes = 0; }
|
|
63
|
+
},
|
|
64
|
+
finish() {
|
|
65
|
+
if (closed) throw new Error('Scheduled stream manifest is already closed');
|
|
66
|
+
closed = true;
|
|
67
|
+
if (unavailable) return { unavailable };
|
|
68
|
+
return { manifest: { version: 1, algorithm: 'sha256', runId: binding.runId,
|
|
69
|
+
sessionId: binding.sessionId, requestId: binding.requestId, dispatchId: binding.dispatchId,
|
|
70
|
+
count: hashes.length, totalBytes, hashes: [...hashes] } };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function validate(manifest, binding) {
|
|
75
|
+
return Boolean(manifest && typeof manifest === 'object' && !Array.isArray(manifest) &&
|
|
76
|
+
Object.keys(manifest).sort().join(',') === 'algorithm,count,dispatchId,hashes,requestId,runId,sessionId,totalBytes,version' &&
|
|
77
|
+
manifest.version === 1 && manifest.algorithm === 'sha256' &&
|
|
78
|
+
['runId', 'sessionId', 'requestId', 'dispatchId'].every(key => id(binding?.[key]) && manifest[key] === binding[key]) &&
|
|
79
|
+
Number.isSafeInteger(manifest.count) && manifest.count >= 0 && manifest.count <= MAX_EVENTS &&
|
|
80
|
+
Number.isSafeInteger(manifest.totalBytes) && manifest.totalBytes >= 0 && manifest.totalBytes <= MAX_TOTAL_BYTES &&
|
|
81
|
+
Array.isArray(manifest.hashes) && manifest.hashes.length === manifest.count &&
|
|
82
|
+
manifest.hashes.every(value => typeof value === 'string' && HASH.test(value)));
|
|
83
|
+
}
|
|
84
|
+
function verify(manifest, binding, envelopes) {
|
|
85
|
+
if (!validate(manifest, binding) || !Array.isArray(envelopes) || envelopes.length !== manifest.count)
|
|
86
|
+
return false;
|
|
87
|
+
let totalBytes = 0;
|
|
88
|
+
try {
|
|
89
|
+
for (let index = 0; index < envelopes.length; index += 1) {
|
|
90
|
+
const entry = fingerprint(envelopes[index]);
|
|
91
|
+
if (entry.hash !== manifest.hashes[index]) return false;
|
|
92
|
+
totalBytes += entry.bytes;
|
|
93
|
+
if (totalBytes > MAX_TOTAL_BYTES) return false;
|
|
94
|
+
}
|
|
95
|
+
} catch { return false; }
|
|
96
|
+
return totalBytes === manifest.totalBytes;
|
|
97
|
+
}
|
|
98
|
+
module.exports = { MAX_EVENTS, MAX_EVENT_BYTES, MAX_TOTAL_BYTES, normalize, fingerprint, create, validate, verify };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ScheduledStreamBinding { runId: string; sessionId: string; requestId: string; dispatchId: string }
|
|
2
|
+
export interface ScheduledStreamManifest extends ScheduledStreamBinding {
|
|
3
|
+
version: 1; algorithm: 'sha256'; count: number; totalBytes: number; hashes: string[];
|
|
4
|
+
}
|
|
5
|
+
export const MAX_EVENTS: number;
|
|
6
|
+
export const MAX_EVENT_BYTES: number;
|
|
7
|
+
export const MAX_TOTAL_BYTES: number;
|
|
8
|
+
export function normalize(envelope: unknown): string;
|
|
9
|
+
export function fingerprint(envelope: unknown): { hash: string; bytes: number; json: string };
|
|
10
|
+
export function create(binding: ScheduledStreamBinding): {
|
|
11
|
+
add(envelopes: unknown): void;
|
|
12
|
+
finish(): { manifest: ScheduledStreamManifest } | { unavailable: string };
|
|
13
|
+
};
|
|
14
|
+
export function validate(manifest: unknown, binding: ScheduledStreamBinding): boolean;
|
|
15
|
+
export function verify(manifest: unknown, binding: ScheduledStreamBinding, envelopes: unknown): boolean;
|