Class: Vangrail::Profile

Inherits:
Object
  • Object
show all
Defined in:
lib/vangrail/profile.rb

Overview

A named, session-pinned posture. Copied from Grok Build's sandbox and permission model, not from a detector paper.

Grok Build pins the sandbox profile for the life of a session and refuses to change it on resume. Deny rules still apply under always-approve. Child processes do not inherit KEY/SECRET/TOKEN. The same four facts live here:

Profile.workspace   cite and search; mutating names are denied
Profile.strict      cite only; only read-only tools
Profile.read_only   no invoke of a mutating tool
Profile.off         no grants

The profile is chosen at Conversation construction and cannot be widened later. Deny always wins over allow and over the plan. Extra allow: / deny: on a named profile is a conflict, not a merge: resolve raises. Deny-wins merge applies when composing from hashes alone.

Constant Summary collapse

NAMES =
%i[off read_only workspace strict].freeze
SECRET =
/key|secret|token|password|passwd|authorization/i

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, allow: {}, deny: [], readonly: false, strip_secrets: true) ⇒ Profile

Returns a new instance of Profile.



30
31
32
33
34
35
36
37
38
39
40
# File 'lib/vangrail/profile.rb', line 30

def initialize(name:, allow: {}, deny: [], readonly: false, strip_secrets: true)
  @name = name.to_sym
  deny_list = Array(deny).map(&:to_s)
  @deny = deny_list.map(&:freeze).freeze
  @allow = allow.transform_keys(&:to_sym)
                .transform_values { |kinds| Array(kinds).map(&:to_sym) }
                .reject { |tool, _| self.class.glob_denied?(tool, deny_list) }
                .freeze
  @readonly = readonly
  @strip_secrets = strip_secrets
end

Instance Attribute Details

#allowObject (readonly)

Returns the value of attribute allow.



28
29
30
# File 'lib/vangrail/profile.rb', line 28

def allow
  @allow
end

#denyObject (readonly)

Returns the value of attribute deny.



28
29
30
# File 'lib/vangrail/profile.rb', line 28

def deny
  @deny
end

#nameObject (readonly)

Returns the value of attribute name.



28
29
30
# File 'lib/vangrail/profile.rb', line 28

def name
  @name
end

#readonlyObject (readonly)

Returns the value of attribute readonly.



28
29
30
# File 'lib/vangrail/profile.rb', line 28

def readonly
  @readonly
end

#strip_secretsObject (readonly)

Returns the value of attribute strip_secrets.



28
29
30
# File 'lib/vangrail/profile.rb', line 28

def strip_secrets
  @strip_secrets
end

Class Method Details

.from_allow(allow, deny: []) ⇒ Object



101
102
103
104
105
106
# File 'lib/vangrail/profile.rb', line 101

def self.from_allow(allow, deny: [])
  deny_list = Array(deny).map(&:to_s)
  cleaned = allow.transform_keys(&:to_sym)
                 .reject { |name, _| glob_denied?(name, deny_list) }
  new(name: :custom, allow: cleaned, deny: deny, readonly: false, strip_secrets: true)
end

.glob_denied?(name, rules) ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
99
# File 'lib/vangrail/profile.rb', line 96

def self.glob_denied?(name, rules)
  needle = name.to_s
  rules.any? { |rule| File.fnmatch?(rule, needle, File::FNM_EXTGLOB) }
end

.named(value) ⇒ Object



86
87
88
89
90
91
92
93
94
# File 'lib/vangrail/profile.rb', line 86

def self.named(value)
  case value.to_sym
  when :off then off
  when :read_only then read_only
  when :workspace then workspace
  when :strict then strict
  else raise ArgumentError, "unknown profile #{value.inspect}"
  end
end

.offObject



55
56
57
# File 'lib/vangrail/profile.rb', line 55

def self.off
  new(name: :off, allow: {}, deny: [], readonly: true)
end

.read_onlyObject



59
60
61
# File 'lib/vangrail/profile.rb', line 59

def self.read_only
  new(name: :read_only, allow: {}, deny: [], readonly: true)
end

.resolve(value, allow: {}, deny: []) ⇒ Object

Raises:

  • (ArgumentError)


77
78
79
80
81
82
83
84
# File 'lib/vangrail/profile.rb', line 77

def self.resolve(value, allow: {}, deny: [])
  extra = !allow.empty? || !Array(deny).empty?
  return from_allow(allow, deny: deny) if value.nil?
  raise ArgumentError, 'pass profile: or allow:/deny:, not both' if extra
  return value if value.is_a?(self)

  named(value)
end

.strictObject



70
71
72
73
74
75
# File 'lib/vangrail/profile.rb', line 70

def self.strict
  new(name: :strict,
      allow: { cite: %i[data] },
      deny: %w[delete_* dump_* shell],
      readonly: true)
end

.strip_secrets(env) ⇒ Object



108
109
110
111
112
113
114
# File 'lib/vangrail/profile.rb', line 108

def self.strip_secrets(env)
  env.each_with_object({}) do |(key, value), kept|
    next if key.to_s.match?(SECRET)

    kept[key] = value
  end
end

.workspaceObject



63
64
65
66
67
68
# File 'lib/vangrail/profile.rb', line 63

def self.workspace
  new(name: :workspace,
      allow: { cite: %i[data], search: [] },
      deny: %w[delete_* dump_* shell],
      readonly: false)
end

Instance Method Details

#denied?(tool) ⇒ Boolean

Returns:

  • (Boolean)


50
51
52
53
# File 'lib/vangrail/profile.rb', line 50

def denied?(tool)
  needle = tool.to_s
  deny.any? { |rule| File.fnmatch?(rule, needle, File::FNM_EXTGLOB) }
end

#readonly?Boolean

Returns:

  • (Boolean)


42
43
44
# File 'lib/vangrail/profile.rb', line 42

def readonly?
  @readonly
end

#strip_secrets?Boolean

Returns:

  • (Boolean)


46
47
48
# File 'lib/vangrail/profile.rb', line 46

def strip_secrets?
  @strip_secrets
end